プログラミングやRPG(作るほう)が好きな人の日記
このウェブページは毎日 夜11時にアクセスできなくなります。 朝6時半に再開されます。(世の中のネット依存対策として) 例外でアクセスができる場合があります。上のメニューの「aboutThisWebsite」を参照してください。 URL は https でもアクセスできます。 黒猫画像はクリック(タッチ)すると消えます。 NO PC WEEK とは私自身の健康のために私のパソコンの使用を制限する期間です。 |
以下の表は、このウェブページの管理人のパソコンの使用時間を管理・制限するためのものです。
No. | A1. 開始時 運動 |
A2. 勉強 1問 |
A3. 終了時 運動 |
H1. 予定 作業内容 |
H2. 予定 作業詳細(進捗%) 判定×の理由 | B. 実際 開始時刻 |
C. 予定 使用時間 (当日限度) |
D. 予定 終了時刻 |
E. 実際 終了時刻 |
F. 実際 使用時間 |
G. 判定 ◎ 9分以下 ○ 10分~19分 △ 20分~29分 × 30分以上 |
---|---|---|---|---|---|---|---|---|---|---|---|
予定 | 記事をカテゴリ分けする | カテゴリをさらに大きな大カテゴリで分ける | |||||||||
予定 | 記事をカテゴリ分けする | 各記事の日付を表示 | |||||||||
予定 | 3Dお姉さんプログラム説明 | 座標を持った2つの要素について片方がもう片方を追尾し続ける試作(この表のNo. 380の問題を受け) | |||||||||
431 | 中止 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(97.9%) もし時間内に終わらない場合は:あきらめる | 18:10 | 2:00(2:00) | 20:10 | 20:21 | 2:11 | ○ | ||
430 | 中止 | 中止 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(97.8%) もし時間内に終わらない場合は:あきらめる | 19:25 | 1:00(4:00) | 20:25 | 20:31 | 1:06 | ◎ | |
429 | 中止 | 中止 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(97.7%) もし時間内に終わらない場合は:あきらめる | 16:25 | 1:30(4:00) | 17:55 | 18:01 | 1:36 | ◎ | |
428 | コロナ接種直後のため中止 | 中止 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(97.6%) もし時間内に終わらない場合は:あきらめる | 13:35 | 1:30(4:00) | 15:05 | 15:14 | 1:39 | ◎ | |
427 | 全体的・基本・簡潔のRPG | (コマンドメニュー作成は中断し、コマンドメニューの戦闘画面対応のために) マップ上でのイベント発生のしくみ(戦闘エンカウントほか)(75%) もし時間内に終わらない場合は:あきらめる | 21:20 | 1:30(1:30) | 22:50 | 22:50 | 1:30 | ◎ | |||
426 | 全体的・基本・簡潔のRPG | (コマンドメニュー作成は中断し、コマンドメニューの戦闘画面対応のために) マップ上でのイベント発生のしくみ(戦闘エンカウントほか)(70%) もし時間内に終わらない場合は:あきらめる | 20:15 | 1:30(1:30) | 21:45 | 22:03 | 1:48 | ○ | |||
425 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(97.5%) ×現状のRPGプログラムを動く状態で掲載しようとしたら、うまくいかなかった。。 もし時間内に終わらない場合は:あきらめる | 14:30 | 1:00(2:00) | 15:30 | 16:18 | 1:48 | × | |||
424 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(97.4%) もし時間内に終わらない場合は:あきらめる | 17:04 | 1:30(UNLOCK) | 18:34 | 18:43 | 1:39 | ◎ | |||
423 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(97.3%) もし時間内に終わらない場合は:あきらめる | 14:20 | 1:30(UNLOCK) | 15:50 | 16:01 | 1:41 | ○ | |||
422 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(97.2%) もし時間内に終わらない場合は:あきらめる | 15:53 | 2:00(UNLOCK) | 17:53 | 18:03 | 2:10 | ○ | |||
421 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(97.1%) もし時間内に終わらない場合は:あきらめる | 13:20 | 1:30(UNLOCK) | 14:50 | 15:00 | 1:40 | ○ | |||
420 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(97%) もし時間内に終わらない場合は:あきらめる | 10:20 | 1:30(UNLOCK) | 11:50 | 11:59 | 1:39 | ◎ | |||
419 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(96.9%) もし時間内に終わらない場合は:あきらめる | 16:52 | 1:30(4:00) | 18:22 | 18:33 | 1:41 | ○ | |||
418 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(96.8%) もし時間内に終わらない場合は:あきらめる | 13:10 | 2:00(4:00) | 15:10 | 15:25 | 2:15 | ○ | |||
417 | × | 全体的・基本・簡潔のRPG | コマンドメニュー作成(96.7%) もし時間内に終わらない場合は:あきらめる | 22:40 | 1:30(1:30) | 24:10 | 24:08 | 1:28 | ◎ | ||
416 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(96.6%) もし時間内に終わらない場合は:あきらめる | 21:20 | 1:30(1:30) | 22:50 | 22:52 | 1:32 | ◎ | |||
記録しなかった長時間作業あり: だめだこりゃ10:30~19:30 長時間になるおそれがあるのはわかっていたのに、それを承知で初めて、案の定、9時間となった。 作業内容は、HTMLの文字部分にCANVASの内容を描こうというもの。 いちおう できはしたけど、なんなんだろう。 それでも、この表による効果は日ごろから上がっていて、プラスの状態だとは思うけど。 | |||||||||||
415 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(96.5%) もし時間内に終わらない場合は:あきらめる |
21:20 | 3:00(UNLOCK) | 24:20 | 24:33 | 3:13 | ○ | |||
記録しなかった長時間作業あり: 自作の便利プログラム「WebBasic」に代わる、JavaScriptの機能を試してしまった。23:30-翌朝5:30 ・CANVASをページ上にに適切な大きさで拡大表示することができる(CSSのobject-fitプロパティ) ・HTML要素がユーザーの目に入ったかどうかを知ることができる「JavaScriptのIntersectionObserver」 ・それらを使用した、プログラムの新しい掲載方法の模索、サンプルプログラム2点の作成と掲載。。。 結果的には、得るものを十分に得たんですが、体の調子は崩れました。 夜通しコンピューターの世界で試行錯誤を重ねて、最後に宝を得る。。これを私はひそかに「ディグダグした」と表現しています。(リンクはナムコの公式動画へ) 宝を見つけるまでの過酷な様子(夜更かし、疲れ、作業の困難さ)と、ディグダグという言葉のゴテゴテとした印象を重ねて。 ディグダグとなった分かれ道のポイントは、今回は「JavaScriptの機能をちょっと試したい」という欲望に自ら乗ったところからでした。 創作の幅を広げることになったので「善」と取りたい気持ちもありますが、翌日に夜更かしの疲れが取れて活動を開始できたのが午後を回ってから、というのを考えると、その欲望を深夜ではなく昼間に行っても結局同じだったのではないだろうか?という疑問がわいてきます。 今月の「記録しなかった長時間作業」は現在3件ありますが、いずれもその原因(太字)に「ちょっと」という言葉を使っているのが特徴的です。 「ちょっと」が爆弾のスイッチ(トリガー)になってるみたいです。 | |||||||||||
414 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(96.4%) もし時間内に終わらない場合は:あきらめる | 21:30 | 1:30(UNLOCK) | 23:00 | 23:02 | 1:32 | ◎ | |||
413 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(96.3%) もし時間内に終わらない場合は:あきらめる ×うまくいかないなと思った | 15:30 | 2:00(UNLOCK) | 17:30 | 19:33 | 4:00 | × | |||
412 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(96.2%) もし時間内に終わらない場合は:あきらめる | 10:50 | 1:30(UNLOCK) | 12:20 | 12:22 | 1:31 | ◎ | |||
411 | × | 全体的・基本・簡潔のRPG | コマンドメニュー作成(96.1%) もし時間内に終わらない場合は:あきらめる | 23:00 | 1:30(1:30) | 24:30 | 0:34 | 1:34 | ◎ | ||
410 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(96%) もし時間内に終わらない場合は:あきらめる | 22:20 | 1:30(1:30) | 23:50 | 0:02 | 1:42 | ○ | |||
409 | グリフォンモデリング | もし時間内に終わらない場合は:あきらめる |
22:00 | 1:00(2:00) | 23:00 | 23:17 | 1:17 | ○ | |||
408 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(95%) もし時間内に終わらない場合は:あきらめる | 13:25 | 1:00(2:00) | 14:25 | 14:35 | 1:10 | ○ | |||
記録しなかった長時間作業あり: レトロゲーム「ソーサリアン」で遊んでしまった。24:00~27:00 最近新しく同じ販売店で「ザ・トリロジーズ」というレトロゲームのセットを購入(予約で発売日未定)しましたが、同じように購入した「ソーサリアン」は購入以来2年半も経っているのに、遊んだ時間は10時間程度でしかありませんでした。それで「ちょっとくらい遊ぶか…」と思って始めたら1時間程度のはずが気のゆるみで長時間(3時間)になってしまいました。 「ソーサリアン」購入目的: 学生時代に好んでいたRPGなので、RPG開発の参考に。 「ザ・トリロジーズ」購入目的: ドラクエ1の真の前身が「夢幻の心臓」(リンクはBingで「ドラクエ 夢幻の心臓」を検索)であり、「ザ・トリロジーズ」に含まれている。RPG開発の参考に。 販売店: EGGプロジェクトのAC-MALL | |||||||||||
407 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(94.6%) もし時間内に終わらない場合は:あきらめる ×うまくいかなくて、あきらめられない!と思った。 そういうときもある。 | 20:15 | 1:00(4:00) | 21:15 | 22:55 | 2:45 | × | |||
406 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(94.5%) もし時間内に終わらない場合は:あきらめる | 15:30 | 1:30(4:00) | 17:00 | 17:01 | 1:31 | ◎ | |||
405 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(94%) もし時間内に終わらない場合は:あきらめる | 12:35 | 1:30(4:00) | 14:05 | 14:14 | 1:39 | ◎ | |||
404 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(93%) もし時間内に終わらない場合は:あきらめる | 20:40 | 1:30(1:30) | 22:10 | 22:10 | 1:30 | ◎ | |||
403 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(92%) もし時間内に終わらない場合は:あきらめる | 21:25 | 1:30(1:30) | 22:55 | 23:04 | 1:39 | ◎ | |||
402 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(90%) もし時間内に終わらない場合は:あきらめる | 21:55 | 1:30(1:30) | 23:25 | 23:35 | 1:40 | ○ | |||
記録しなかった長時間作業あり: 作業内容:
タブ文字によるスペース挿入(等間隔に整列)は文章の文字数をうまく数えればできるだろうと甘く考えたが、ユニコードという文字コードは1文字3バイトになっていて、perlのlength(文字列)だとバイト数を数えるので「3文字」で計算してしまう。文章中で半角文字とユニコード文字(=漢字。横幅が半角2文字分)が混在しているとき、1文字ずつ取り出してユニコードなら2文字、そうでないなら1文字という計算をすることになってしまった。その際の「1文字ずつ取り出す」というのもそのままだと「1バイト取り出す」になってしまうので、文字コードの変換(ユニコード文字をperlの内部文字コードに変換すれば1バイトではなく1文字で取り出せる)をいちいち行う必要があった。
環境の実際の仕様と、プログラマーが勝手に思っている仕様にギャップがあると、泥沼にはまる、というのは一般的にこの先ずっと続くことなのだろうか…。 その成果のプログラムリストがコレ(リンクはスクリーンショットです。perlの正規表現を一部で使っているので意味不明に見えるところもあると思います)
| |||||||||||
401 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(85%) もし時間内に終わらない場合は:あきらめる | 19:15 | 2:30(4:00) | 21:45 | 21:42 | 2:27 | ◎ | |||
400 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(80%) もし時間内に終わらない場合は:あきらめる |
15:35 | 1:30(4:00) | 17:05 | 17:28 | 1:53 | △ | |||
399 | 3Dお姉さんプログラム説明 | 3Dお姉さんプログラム説明は現在2件あるが、そのうち1件が動いていないので、それを直す。(100%) 直したページ (このリンク先で表示される くねくね動く画面の「PROGRAM LIST」というボタンを押すと「3Dお姉さんプログラム説明」が開きます。それが今まで動作しませんでしたが今回直しました。ただし内容はあまり親切ではありません。ブラウザのキャッシュが きいていると古いファイルを読んでしまい、直したものが反映されないことがあります。動かないなと思ったらブラウザの再読み込みボタンを押してください) もし時間内に終わらない場合は:あきらめる |
21:10 | 0:30(1:30) | 21:40 | 21:52 | 0:42 | ○ | |||
398 | 全体的・基本・簡潔のRPG | コマンドメニュー作成(75%) もし時間内に終わらない場合は:あきらめる |
21:00 | 1:30(1:30) | 22:30 | 22:37 | 1:37 | ◎ |
この表の意図:
多くの人はパソコンのやりすぎやネットゲームのやりすぎには困っていると思います。
参考に言うと、この表を使う前の私は 1 回の PC 使用時間がノンストップで 17 時間というときもあったし、平均で言うと毎日 9 時間はやっていたと思います。
この表を使ってパソコンの使用時間を 事前に決めて、ネット上に公開 することで、パソコンのやりすぎを防止できたら、と思います。
また、数年前から考えてきましたが、そういう徹夜とか長時間作業をするよりも、昼間の短時間作業のほうが生産性は高いのではないかと思います。そういう意味でも期待しています。
※以前は NO PC WEEK と称してパソコンを使用しない期間を設けることでやりすぎに対処してきましたが、もっと具合の良い方法はないかと考え、この表を使うようになりました。
記入の法則:
ちなみに、分単位で記録を取ったりして、だいぶマメに見えるかもしれませんが、Windows の日本語入力(MS-IME)で「いま」と入力し、 変換 キーを押さずに ↑ ボタンを押すと現在の時刻になります。道具の便利さが人をマメに見せるのかもしれません。
例外事項:
「スーパーPC WEEK」:
連休中(3連休以上)に、NO PC WEEK をオフにして好きなだけパソコンを使ってよいとする期間を、「NO PC WEEK」に対して「スーパーPC WEEK」と言う。
ただし以下の決まりを守ること。
なお、表の中央やや右寄りの「C. 予定 使用時間(当日限度)」列の "当日限度" には UNLOCK と記入する。
中途結果:
結構いい結果になっています。炊事や掃除、散歩、早起きなどが好ましいリズムでできるようになりました。
(2020年9月6日追記:散歩、早起きは最近あまりできていません。掃除や炊事は理想的にできています)
また、パソコン以外の趣味も進むようになりました。電子回路、ガンプラ、RPG のプログラム以外のモンスターイラストやストーリーなど RPG の肉付け部分の創作、勉強、昔好きだったペーパークラフト等々。
そしてパソコンの趣味自体も深夜遅くまで行うよりも質が高くなったように感じます。制限された短い時間の中で結果を得ようとするので、取捨選択が行われているし、時間が終了して、空いた時間ができ、それがほどよい休憩になり、今後の作業の方針を落ち着いて検討することもできます。それが質につながっているのかなと思います。
この取り組みが、15 才 ~ 18 才くらいまでの高専(中退)に所属していた時に実施できていたら良かっただろうなと思います。でもそれくらいの年齢では経験が浅く、このような効果的なルール作りを行うことはできなかったと思います。自分は人に比べて「創作意欲」や「ゲームで遊ぶ欲望」におぼれやすいところがあり、そのコントロールはとても難しいです。
自分の家族にすすめたい方へ:
パソコンのやりすぎやネットゲームのやりすぎは社会問題にもなっているので、「うちの子についてなんとかしないと…」と思っているご家族の方は多くいらっしゃると思います。
私の両親も過去に私について問題にしていました。学校へ行かず、毎日朝までパソコンに向かい、悶々としていたんです。
この表はその家族が困っていたときから 30 年後に、私が自分で必要を感じて作ったものです。
私は今 一人暮らしをしていて、自分で生計を立てる中、パソコンにおぼれた生活をすると、生活がうまく回らなくなるんです。
具体的には、
これらを改善するために表を作りました。
でも、このような必要にせまられて「自分の動機で始めた場合」と、「人からすすめられて始めた場合」とでは、結果が異なると思います。
自分の切実な動機で始めたなら自分から進んでこの表を活用すると思いますが、外から押し付けられたものはなかなか定着しないものです。
あまり適当なことは言えませんが、「中途結果」タブの中の青い部分で書いたことは、本人にとって得になることなので、「ときどき休憩して、他のあの趣味やってみたらどうだ?」とか「ときどき休憩したほうがプログラミングの質が上がるって話だぞ?」という形ですすめてみたらどうでしょうか。(それでも最終的には自立してもらうことは必要だと思いますが)
私が両親を困らせていたときに、突然、外へ一人で出て行って、一人暮らしを始めたり、接客業を始めたり、いくつか資格取得したりといろいろ行えた理由というのは、正直言ってわかりません。(※しかし途中で失業して2度、実家に戻ったことがあります。1 回目は 21 才くらいのときに 5 年間、2 回目は 35 才くらいのときに1年未満、実家にいて、何もしてなかったり働いたりしていました)
私が両親を困らせていたのは 16 才 ~ 20 才くらいの学生のころですが、そのころ家族と私自身と友人たちがみんなそれぞれ、私の生活について心配したり困ったり悩んだり、あの手この手を試したりしていました。そういう煮詰まったような状況が運命をそのように(解決の方向へ)動かすのかもしれません。運命がどうの というのは変ですが、そのくらいのことしか言えません。何かしら取り組む必要があるということですかね。
この社会問題はクリアーすべきものみたいです。
最近私が趣味で作っている RPG の試作品の、動くものを掲載します。
ただし、キーボードが必要で、タッチデバイスでは動かせません。
この画像リンクをクリックすると新しいウィンドウを開いて JavaScript を実行します。
※Google Chrome で描画がおかしくなっていたようですが、修正しました。ただしブラウザの「キャッシュ」が機能していると修正前のプログラムのままになってしまいます。キャッシュを読まないでそのページを読み取る方法は…
Windows | Mac | |
Google Chrome | SHIFT + F5 | Command + Shift + R |
Fire fox | CTRL + F5 | Command + Shift + R |
Webで調べて、私自身では試していない情報なので、間違っていたらスミマセン。
そのプログラムリストです。全行数は 1838 行。
この青い部分をダブルクリックするたびに、リストの全体を表示/非表示します。
タッチデバイスの場合はタッチします。
/* memo.
名前のすみわけ(理想であって、実際はそうなっていないかもしれない)
"イベント"
イベントドリブンのイベント イベント
シナリオスクリプトのイベント ストーリー
"アイテム"
もちもの トレジャー
メニュー項目 アイテム
座標
グラフィクス gx, gy, gw, gh
マス目のある画面 col, row, cols, rows
データ上の位置 x, y, w, h
配列変数の名前と、同じデータを持つ連想配列変数の名前
配列 -s たとえばcharacters
連想配列 -z たとえばcharacterz
そのとき、たとえばsprites、spritezの読み方について
配列時 sprites スプライトス
連想配列時 spritez スプライトス
※どちらも-スと読む。つまり-zは見た目の区別でしかない
連想配列のkeyとキー入力のkey
連想配列 name
キー入力 key
言葉の意味
tweak 調整する
*/
// |
// V
class Utl {//c
static arrayForward( array, element ) {//m
let idx = array.indexOf( element );
//check.
if( idx == -1 ) {
alert( "Utl.arrayForward() 失敗した" );
return;
}
array.splice( idx, 1 );
array.push( element );
}
static accessBy( object /*keys*/ ) {//m
for( let i = 1; i < arguments.length; i++ ) {
object = object[ arguments[ i ] ];
if( object == null ) return null;
if( typeof object === "undefined" ) return undefined;
}
return object;
}
static array2object( array, object, keyMaker ) {//m
for( let element of array ) {
object[ keyMaker( element ) ] = element;
}
}
static objectFrom( array, keyMaker ) {//m
let object = new Object();
for( let element of array ) {
object[ keyMaker( element ) ] = element;
}
return object;
}
static lengthBin( str ) {//m
//漢字を2バイト、半角を1バイトとして計算 (UTF8を前提)
return encodeURI( str ).replace( /%20/, "*" ).replace( /%..%..%../g, "**" ).length;
}
static lengthBinDiv2( str ) {//m
//lengthBの結果を2で割り、小数点切り上げ
return Math.ceil( Utl.lengthBin( str ) / 2 );
}
static maxLength( strArray ) {//m
let v = 0;
for( let str of strArray ) v = Math.max( v, String( str ).length );
return v;
}
static maxLengthBin( strArray ) {//m
let v = 0;
for( let str of strArray ) {
v = Math.max( v, Utl.lengthBin( String( str ) ) );
}
return v;
}
static maxLengthBinDiv2( strArray ) {//m
return Math.ceil( Utl.maxLengthBin( strArray ) / 2 );
}
static objectCopy( object ) {
let copy = new Object();
for( let key in object ) {
copy[ key ] = object[ key ];
}
return copy;
}
}//Utl
class Element {//c
constructor( style, app ) {
this.style = style;
this.app = app ? app : this;
//check.
if( this.style.cellSize === undefined ) {
this.style.cellSize = this.app.style.cellSize;
}
this.name = this.constructor.name;
this.x = this.style.col * this.style.cellSize;
this.y = this.style.row * this.style.cellSize;
this.w = this.style.cols * this.style.cellSize;
this.h = this.style.rows * this.style.cellSize;
if( this.style.cellSize == undefined )
this.style.cellSize = 5; //異常をわかりやすくするために5
if( this.style.fontSize == undefined )
this.style.fontSize = this.style.cellSize;
this.childs = new Array();
this.floats = new Array();
}
typeKey( key ) {//m
}
senseKey( key ) {//m
}
draw( cc ) {//m
this.drawWindow( cc );
this.drawChildren( cc );
}
drawWindow( cc ) {
//ウィンドウ
let x = this.x - this.style.cellSize / 2;
let y = this.y - this.style.cellSize / 2;
let w = this.w + this.style.cellSize;
let h = this.h + this.style.cellSize;
//ウィンドウ 面
cc.fillStyle = "white";
cc.fillRect( x, y, w, h );
//ウィンドウ 枠
if( this.app.currentElement == this )
cc.strokeStyle = "yellow";
else
cc.strokeStyle = "black";
cc.strokeRect( x, y, w, h );
}
drawChildren( cc ) {
for( let child of this.childs ) child.draw( cc );
for( let float of this.floats ) float.draw( cc );
}
searchStory() {//m
}
focus() {
/*
下記記述でもよいが、focus()と書いたほうがわかりやすいと思って。
*/
this.app.currentElement = this;
}
appendChild( child ) {//m
if( child.style.isFloat ) {
this.floats.push( child );
} else {
this.childs.push( child );
child.focus();
}
child.parent = this;
return child;
}
removeChild( child ) {//m
let idx;
console.log( "remove", child.name, "from", this.name );
//floatの場合、floatsから削除
if( ( idx = this.floats.indexOf( child ) ) > -1 ) {
this.floats.splice( idx, 1 );
} else if( ( idx = this.childs.indexOf( child ) ) > -1 ) {
//childの場合、childsから削除
this.childs.splice( idx, 1 );
}
//メッセージウィンドウの場合はthis.appからも削除
if( child == this.app.messageWindow ) {
delete this.app.messageWindow;
}
//削除したものがカレントだった場合、バトンタッチ
if( child == this.app.currentElement ) {
let nextCurrent = this.searchNextCurrent();
nextCurrent.focus();
console.log( "next", nextCurrent.name );
}
return child;
}
// |
// V
searchNextCurrent() {//m
/*
親子関係において、子が閉じられたとき、フォーカスをどの要素に移すか。
removeChildから呼ばれる。
*/
if( this.childs.length ) {
return this.childs[ this.childs.length - 1 ];
} else {
return this;
}
}
}//Element
// |
// V
class App extends Element {//c
constructor( canvasId ) {
let canvas = document.getElementById( canvasId );
canvas.width = 256;
canvas.height = 224;
let cellSize = 16;
super( {
col : 0,
row : 0,
cols : Math.floor( canvas.width / cellSize ),
rows : Math.floor( canvas.height / cellSize ),
cellSize : cellSize,
}, null );
this.cc = canvas.getContext( "2d", { alpha : false } );
//---メトロノーム
this.metronomez = new Object();
this.addMetronome( "t1000_i2" );
this.addMetronome( "t500_i2" );
this.addMetronome( "t250_i2" );
//カーソル専用(手動追加)
this.metronomez.forCursor = {
maxTime : 500,
maxIndex : 0,
time : 0,
index : 0,
toggle : true,
changed : false,
}
/*
メトロノームの各フラグの変化タイミング図解(タイミングチャート)
maxTime : 250,
maxIndex : 3, の場合の各フラグの動き↓
(経過時間) 0 ... 250 ... 500 ... 750 ... 1000 ...(ms)
time : 0, 0 ..249 0 ..249 0 ..249 0 ..249 0 .. ...(ms)
index : 0, 000000001111111122222222000000001111 ...
toggle : true, ~~~~~~~~________~~~~~~~~________~~~~ ...
changed : false, ________~_______~_______~_______~___ ...
~ : true
_ : false
... : 略
※このグラフ中の1文字単位の縦1列はrequestAnimationFrameに相当する
つまり、requestAnimationFrameごとに、
timeは経過時間を増やしていきmaxTimeになると0に戻る
indexはtimeがmaxTimeになるごとに1増え、maxIndexになると0に戻る
toggleはtimeがmaxTimeになるごとに真偽を反転する
changedはtimeがmaxTimeになったときだけ真になる
maxTimeでアニメのパラパラのスピード決め
toggleやchangedでアニメのパラパラの実行を判断
indexでアニメのパラパラの画の切り替え
*/
this.spritez = {
player : {
images : [ "←", "-", "↑", "|", "→", "-", "↓", "|" ],
metronome : this.metronomez.t500_i2,
},
}
this.anms = new Array();
this.artz = {
spritez : this.spritez,
bgms : new Object(),
soundz : new Object(),
}
this.screens = new Array();
this.screenz = new Object();
this.characters = new Array();
this.characterz = new Object();
this.treasureCatalog = new Object();
//---メニュー
this.menusrcs = [
{
name : "yesno",
title : "",
items : [
{
name : "はい",
},
{
name : "いいえ",
}
],
},
{
name : "camp",
title : "コマンド?",
argzkey : "cmd",
items : [
{
name : "どうぐ",
title : "だれの??",
items : this.characters,
argzkey : "char1",
itemsMenu : {
title : "どれ?",
argzkey : "dougu",
row : 3, //だれでも表示高さを固定して表示
},
},
{
name : "しらべる",
},
{
name : "まほう",
title : "だれの?",
items : this.characters,
argzkey : "char1",
},
],
},
//---
{
name : "つかう",
title : "だれに?",
items : this.characters,
argzkey : "char2",
script : async function( argz ) {
await this.tell( argz.char1.name + "は" + argz.char2.name + "に" + argz.dougu.name + "を使った!", { wait : true, close : true } );
let lostFlg = true;
if( argz.dougu.limit != undefined ) {
//limitが0になったらなくなるタイプ
argz.dougu.limit --;
//check.
lostFlg = argz.dougu.limit == 0;
if( lostFlg ) {
await this.tell( "なんと " + argz.dougu.name + " は壊れてしまった!", { wait : true, close : true } );
}
}
if( lostFlg ) {
let idx = argz.char1[ argz.cmd.name ].indexOf( argz.dougu );
argz.char1[ argz.cmd.name ].splice( idx, 1 );
}
},
},
{
name : "わたす",
title : "だれに?",
items : this.characters,
argzkey : "char2",
script : async function( cmd, char1, dougu, cmd2, char2 ) {
await this.tell( char1.name + "は" + char2.name + "に" + dougu.name + "を渡した!", { wait : true, close : true } );
},
},
{
name : "すてる",
script : async function( cmd, char1, dougu, cmd2 ) {
await this.tell( char1.name + "は" + dougu.name + "を捨てた!", { wait : true, close : true } );
},
},
]
this.menusrcz = Utl.objectFrom( this.menusrcs, element => element.name );
//---トレジャー
this.treasureCatalog = {
"どうけん" : {
},
"せいどうけん" : {
},
"てっけん" : {
},
"こうてつけん" : {
},
"ひほう" : {
limit : 3,
contextMenu_camp : {
title : "どうする?",
items : [
this.menusrcz[ "つかう" ],
this.menusrcz[ "わたす" ],
this.menusrcz[ "すてる" ],
],//items
},
},
"なんこう" : {
contextMenu_camp : {
title : "どうする?",
argzkey : "dousuru",
items : [
this.menusrcz[ "つかう" ],
this.menusrcz[ "わたす" ],
this.menusrcz[ "すてる" ],
],//items
},//nextMenu
contextMenu_battle : this.menusrcz[ "つかう" ],
},//なんこう
}//treasureCatalog
//tweak. キーを名前として要素内に登録
for( let key in this.treasureCatalog ) {
this.treasureCatalog[ key ].name = key;
}
//---キャラクター
this.characters.push( {
name : "キャラ1",
"どうぐ" : [
{ name : "つるぎ" },
this.treasureCatalog[ "なんこう" ],
Utl.objectCopy( this.treasureCatalog[ "ひほう" ] ),
{ name : "つるぎ" },
],
} );
this.characters.push( {
name : "キャラ2",
"どうぐ" : [
{ name : "なんこう2" },
{ name : "つるぎ" },
],
} );
this.characters.push( {
name : "キャラ3",
"どうぐ" : [
{ name : "なんこう3" },
{ name : "つるぎ" },
],
} );
Utl.array2object( this.characters, this.characterz, element => element.name );
let dir = location.pathname.replace( /%20/g, " " ).replace( /\/[a-z0-9_ .-]*$/i, "" ) + "/";
//素材のプリロード
if( 1 ) {
return Promise.all( [
this.load( "bgm", "field", dir + "_bgm/街/MusMus-CT-NV-24/MusMus-CT-NV-24.mp3" ),
this.load( "bgm", "town", dir + "_bgm/街/MusMus-BGM-078/MusMus-BGM-078.mp3" ),
// this.load( "bgm", "battle", dir + "_bgm/battle.wav" ),
this.load( "sound", "pushA", dir + "_sound/pushA.mp3" ),
this.load( "sound", "pushWall", dir + "_sound/pushWall.mp3" ),
this.load( "sound", "walk", dir + "_sound/walk.wav" ),
] ).then( function() {
return this; //constructorだからインスタンスを返すこと
}.bind( this ) );
} else {
return Promise.all( [
this.load( "bgm", "field", dir + "_bgm/10 イルバーンズの遺跡 (遺跡・地上).wav" ),
this.load( "bgm", "town", dir + "_bgm/1-05 街.wav" ),
this.load( "bgm", "battle", dir + "_bgm/battle.wav" ),
this.load( "sound", "pushA", dir + "_sound/pushA.mp3" ),
this.load( "sound", "pushWall", dir + "_sound/pushWall.mp3" ),
this.load( "sound", "walk", dir + "_sound/walk.wav" ),
] ).then( function() {
return this; //constructorだからインスタンスを返すこと
}.bind( this ) );
}
}//constructor()
// |
// V
//プリロード
load( type, name, src ) {//m
return new Promise( function( endmark ) {
let object;
console.log( type, name, "loading.." );
switch( type ) {
case "bgm":
object = new Audio();
object.oncanplaythrough = function() {
this.artz.bgms[ name ] = object;
object.oncanplaythrough = null;
console.log( "\t", type, name, "loaded." );
endmark();
}.bind( this );
object.src = src;
object.load();
break;
case "sound":
object = new Audio();
object.oncanplaythrough = function() {
this.artz.soundz[ name ] = object;
object.oncanplaythrough = null;
console.log( "\t", type, name, "loaded." );
object.rapidPlay = function() {
object.pause();
object.currentTime = 0;
object.play();
/*
同じ音楽ファイルの連続演奏時は
最初にリセットをかける必要がある。
rapid=急速
*/
}
endmark();
}.bind( this );
object.src = src;
object.load();
break;
}
}.bind( this ) );
}//load()
// |
// v
stop() {//m
cancelAnimationFrame( this.timerId );
}
start() {//m
//------------
this.focus();
this.keys = new Array();
onkeydown = function( e ) {
if( this.keys.indexOf( e.which ) == -1 ) {
this.keys.push( e.which ); //キーセンス オン
this.currentElement.typeKey( e.which );
//キータイプ 実行
}
}.bind( this );
onkeyup = function( e ) {
let idx = this.keys.indexOf( e.which );
if( idx > -1 ) this.keys[ idx ] *= -1;
//キーセンス オフ要求(同値のマイナスで示す)
}.bind( this );
this.status = {
player : {
position : {
map : App.worldmap,
x : 0,
y : 0,
},
direction : 0,
gold : 200,
},
}
//タイトル画面から始める
let titleScreen = new TitleScreen( {
col: 0,
row : 0,
cols : this.style.cols,
rows : this.style.rows,
cellSize : 8,
}, this );
this.appendChild( titleScreen );
this.beforeTime = 0;
this.frame( 0 );
}//constructor()
// |
// V
frame( tm ) {//m
//経過時間
let diff = tm - this.beforeTime;
this.beforeTime = tm;
//メトロノーム更新
for( let key in this.metronomez ) {
let metronome = this.metronomez[ key ];
metronome.time += diff
//check. そのメトロノームは時を刻んだ
if( metronome.time >= metronome.maxTime ) {
metronome.time = 0;
metronome.toggle = ! metronome.toggle;
metronome.changed = true;
metronome.index ++;
//check. インデックスリセット
if( metronome.index == metronome.maxIndex ) metronome.index = 0;
} else {
metronome.changed = false;
}
}
//キーセンス 処理
for( let i = this.keys.length - 1; i >= 0; i-- ) {
let key = this.keys[ i ];
this.currentElement.senseKey( Math.abs( key ) );
//check. キーセンス オフ
if( key < 0 ) this.keys.splice( i, 1 );
}
//1ドットスクロール実行
if( this.mapScreen )
if( this.mapScreen.scroll.isAnimating ) {
this.mapScreen.frameForScroll();
}
//ストーリー検索&実行
let story;
if( story = this.currentElement.searchStory() ) {
story.begin.call( this );
}
//アニメ処理 (forの途中で要素削除するので逆順)
for( let i = this.anms.length - 1; i >= 0; i-- ) {
if( this.anms[ i ]() ) this.anms.splice( i, 1 );
}
this.draw( this.cc );
this.timerId = requestAnimationFrame( this.frame.bind( this ) );
}
// |
// V
typeKey( key ) {//m
//check.
if( key == 80 ) {
if( this.timerId ) {
cancelAnimationFrame( this.timerId );
this.timerId = null;
this.cc.fillStyle = "yellow";
this.cc.fillText( "PAUSE", 100, 100 );
} else {
this.frame();
}
}
}
async tell( message, flgs ) {//m
//check. メッセージウィンドウがないときは作成する
if( ! this.messageWindow ) {
this.messageWindow = new MessageWindow( this.app );
this.currentElement.appendChild( this.messageWindow );
}
this.messageWindow.focus();
await this.messageWindow.write( ...arguments );
}
// |
// V
yesno( closeFlg ) {//m
this.artz.soundz.pushA.rapidPlay();
let yesnoMenu = new Menu( "selectmode", "", this.menusrcz.yesno, {
col :27,
row : 24,
}, this );
this.mapScreen.appendChild( yesnoMenu );
return new Promise( function( endmark ) {
yesnoMenu.endmark = endmark;
}.bind( this ) ).then( function( selectedItem ) {
this.mapScreen.removeChild( yesnoMenu );
if( selectedItem )
return selectedItem.name == "はい";
else
return false;
}.bind( this ) );
}
// |
// V
async shop() {//m
let treasureMenu = new Menu( "selectmode", "", {
name : "shop",
title : "刀剣屋 'ロバートウッドテイル'",//全13, 半3
items : [
{ name : this.treasureCatalog[ "どうけん" ].name, price : 100, },
{ name : this.treasureCatalog[ "せいどうけん" ].name, price : 200, },
{ name : this.treasureCatalog[ "てっけん" ].name, price : 300, },
{ name : this.treasureCatalog[ "こうてつけん" ].name, price : 400, },
],
columns : [
{
key : "name",
},
{
key : "price",
align : "right",
}
],
}, {
col : 2,
row : 2,
}, this );
this.mapScreen.appendChild( treasureMenu );
//所持金表示
let stat = new List( {
columns : [ { key : "title" }, { key : "value", align : "right" } ],
items : [
{
title : "エン",
value : function() { return this.status.player.gold; },
},
],
}, {
col : treasureMenu.style.col + treasureMenu.style.cols + 2,
row : treasureMenu.style.row,
isFloat : true,
}, this.app );
treasureMenu.appendChild( stat );
while( 1 ) {
await this.tell( "何を買うんだ?\n" );
//商品を選ぶ
let selectedItem = await new Promise( function( endmark ) {
treasureMenu.endmark = endmark;
treasureMenu.focus();
}.bind( this ) );
//check. お店から退出
if( ! selectedItem ) break;
await this.tell( selectedItem.name + "だな。\n" );
await this.tell( selectedItem.price + "エンだ。買うかね?\n" );
if( await this.yesno() ) {
this.status.player.gold -= selectedItem.price;
await this.tell( "まいどあり!\n", { wait : true } );
await this.tell( "\n" );
}
}
await this.tell( "ありがとうございました!", { wait : true, close:true } );
await this.mapScreen.removeChild( treasureMenu );
}
addMetronome( name /*other args*/ ) {//m
//オーバーロード(メソッド名が同じで引数のパターンが異なる)の模造
//”関数の先頭で引数の型を判定する条件分岐で対応”
let argsId = "";
for( let arg of Array.from( arguments ) ) {
argsId += "," + ( typeof arg ).substr( 0, 1 );
}
let metronome;
switch( argsId ) {
case ",s" : //フォーマット名称 t時間_iインデックス上限
let tokens = name.split( /_/ );
metronome = {
maxTime : Number( tokens[ 0 ].substr( 1 ) ),
maxIndex : Number( tokens[ 1 ].substr( 1 ) ),
time : 0,
index : 0,
toggle : false,
changed : false,
}
this.metronomez[ name ] = metronome;
break;
case ",s,n" : //自由な名称、時間
let tm = arguments[ 1 ];
metronome = {
maxTime : tm,
maxIndex : 0,
time : 0,
index : 0,
toggle : false,
changed : false,
}
this.metronomez[ name ] = metronome;
break;
default:
alert( "addMetronome()にて、未定義の引数リスト:[" + args + "]" );
}
return metronome;
}//addMetronome()
}//App
class MessageWindow extends Element {//c
constructor( app ) {
super( {
col : 1,
row : 12,
cols : 19,
rows : 5,
cellSize : 12,
}, app );
this.matrix = new Array();
for( let r = 0; r < this.style.rows; r++ ) {
this.matrix.push( new Array() );
}
this.matrixCursorCol = 0;
this.matrixCursorRow = 0;
//フラグ
this.endedByOutside = false;
}
write( message, flgs ) {//m
//check.
if( ! flgs ) {
flgs = {
wait : false, //メッセージ表示後キー入力待ちしたいとき
close : false, //メッセージ表示後ウィンドウを閉じたいとき
}
}
//check. "外部により終了させられた"フラグ(1文字ずつ表示を一気に等)
this.endedByOutside = false;
this.message = message;
this.flgs = flgs;
return new Promise( function( endmark ) {
this.endmark = endmark;
this.metronome = this.app.addMetronome( "test1", 100 );
this.seek = 0;
//1文字ずつ表示するアニメーション
this.app.anms.push( function() {
//check. メトロノームに合わせる
if( ! this.metronome.changed ) return;
//終了チェック兼1文字出力
if( this.endedByOutside || ! this.writeChar() ) {
//check. キー待ちしない
if( this.flgs.wait == false ) this.endmark();
//キー待ちする場合は、typeKey()のほうでthis.endmark()される
return true; //アニメ終了の意
}
}.bind( this ) );//anm
}.bind( this ) ).then( function() {
//endmark()されたらここに来る
//MessageWindowのclose指定
if( this.flgs.close ) {
this.parent.removeChild( this );
}
}.bind( this ) );//new Promise
}//write()
// |
// V
writeChar() {//m
/* memo.
messageの末尾時にfalseを返す
*/
let ch = this.message.substr( this.seek, 1 );
let kaigyo = false;
switch( ch ) {
case "\n":
kaigyo = true;
break;
default:
this.matrix[ this.matrixCursorRow ][ this.matrixCursorCol ] = ch;
this.matrixCursorCol ++;
//check. 右端
if( this.matrixCursorCol == this.style.cols ) kaigyo = true;
}//switch
//check. 改行
if( kaigyo ) {
this.matrixCursorCol = 0;
//ウィンドウの途中行
if( this.matrixCursorRow < this.style.rows - 1 ) {
this.matrixCursorRow ++;
} else {
//ウィンドウの最下行
//1行スクロール
this.matrix.shift();
this.matrix.push( new Array() );
}
}
this.seek ++;
return this.seek < this.message.length; //messageの末尾時false
}//writeChar()
typeKey( key ) {
if( this.seek < this.message.length ) {
//文字出力中のときは文字出力を一気に行う
for( let i = this.seek; i < this.message.length; i++ ) {
this.writeChar();
}
this.endedByOutside = true;
} else if( this.flgs.wait ) {
//文字出力終了のときは、呼び出し元に処理終了を知らせる。
this.endmark();
}
}//typeKey()
draw( cc ) {
super.draw( cc );
cc.save();
cc.translate( this.x, this.y );
cc.font = this.style.fontSize + "px ''";
cc.fillStyle = "black";
for( let r = 0; r < this.style.rows; r++ ) {
let gy = this.style.fontSize * r;
for( let c = 0; c < this.style.cols; c++ ) {
let gx = this.style.fontSize * c;
let ch = this.matrix[ r ][ c ];
//check.
if( ! ch ) continue;
cc.fillText( ch, gx, gy + this.style.fontSize );
}
}
cc.restore();
}//draw()
}//MessageWindow
// |
// V
//---●List
class List extends Element {//c
constructor( src, style, app ) {
//check.
if( style.cellSize == undefined ) style.cellSize = 8;
//check. 行数と列数を計算
if( style.cols == undefined ) {
style.cols = 0;
//各列において、その全行の文字列の中での最大文字列数を得る
//その最大文字列数をstyle.colsへ加算していき、ウィンドウの横幅とする
for( let column of src.columns ) {
let strings = new Array();
for( let item of src.items ) {
let string = item[ column.key ] == undefined ? item.ref[ column.key ] : item[ column.key ];
if( string instanceof Function ) string = string.call( app );
strings.push( string );
}
column.cols = Utl.maxLengthBinDiv2( strings ) + 1; //その列の幅
column.col = style.cols; //その列の位置
style.cols += column.cols;
}
//check. タイトルのほうが長ければ
if( src.title != undefined ) {
style.cols = Math.max( Utl.lengthBinDiv2( src.title ), style.cols );
}
//check.
if( style.paddingLeft != undefined ) style.cols += style.paddingLeft;
}//if
if( style.rows == undefined ) {
style.rows = src.items.length;
//check.
if( src.title ) style.rows++;
}
super( style, app );
this.src = src;
}
draw( cc ) {//m
this.drawWindow( cc );
//debug. テキストエリアに水色枠
if( 0 ) {
cc.strokeStyle = "cyan";
cc.strokeRect( this.x, this.y, this.w, this.h );
}
let gx, gy, gw, gh;
cc.save();
//原点を描画位置へ移動
gx = this.style.col * this.style.cellSize;
gy = this.style.row * this.style.cellSize;
cc.translate( gx, gy );
cc.fillStyle = "black";
cc.font = this.style.fontSize + "px ''";
//タイトル
if( this.src.title ) {
gx = ( this.style.paddingLeft - 0.3 ) * this.style.fontSize;
gy = 0.65 * this.style.fontSize;
cc.fillText( this.src.title, gx, gy );
}
this.translateToItemArea( cc );
for( let i = 0; i < this.src.items.length; i++ ) {
let item = this.src.items[ i ];
let gy = this.style.cellSize * i;
for( let col = 0; col < this.src.columns.length; col++ ) {
let column = this.src.columns[ col ];
let str = item[ column.key ];
//check.
if( str instanceof Function ) str = str.call( this.app );
let gx = column.col * this.style.cellSize;
if( Utl.accessBy( column, "align" ) == "right" ) {
gx += column.cols * this.style.cellSize;
gx -= cc.measureText( str ).width;
}
//check.
if( typeof str === "undefined" ) {
str = item.ref[ column.key ];
}
cc.fillText( str, gx, gy + this.style.cellSize );
//debug. 列に緑枠
if( 0 ) {
cc.strokeStyle = "green";
cc.strokeRect(
column.col * this.style.cellSize,
gy,
column.cols * this.style.cellSize,
this.style.cellSize
);
}
}//for
}//for
cc.restore();
}//draw()
translateToItemArea( cc ) {//m
if( this.src.title ) cc.translate( 0, this.style.fontSize );
cc.translate( this.style.paddingLeft * this.style.cellSize, 0 );
}
}//List
// |
// V
//---●Menu
class Menu extends List {//c
constructor( mode, context, src, style, app ) {
//check.
if( src.columns == undefined ) {
src.columns = [
{
key : "name",
},
]
}
//check.
if( style.paddingLeft == undefined ) style.paddingLeft = 1;
super( src, style, app );
this.mode = mode;
this.context = context;
this.src = src;
//check.
if( this.src.context ) this.context = this.src.context;
this.itemCols = 1;
this.itemRows = this.src.items.length;
this.cursorX = 0;
this.cursorY = 0;
}
typeKey( key ) {//m
let addX = ( key == 39 ) - ( key == 37 );
let addY = ( key == 40 ) - ( key == 38 );
if( addX || addY ) {
this.cursorX += addX;
this.cursorY += addY;
//check.
if( this.cursorY < 0 ) this.cursorY = 0;
if( this.cursorY >= this.itemRows ) this.cursorY = this.itemRows - 1;
//カーソル 動いた時点で「点滅の点灯開始状態」にする
//こうしないとチラチラして見づらい
this.app.metronomez.forCursor.toggle = true;
this.app.metronomez.forCursor.time = 0;
} else {
switch( key ) {
case 32://SPACE
this.app.artz.soundz.pushA.rapidPlay();
this.selectedItem = this.src.items[ this.cursorY ];
//選択した項目の形態
let menusrc;
//選択した項目はメニューであるA
if( this.selectedItem.items ) {
menusrc = this.selectedItem;
} else if( this.src.itemsMenu ) {
//選択した項目はメニューではないが、
//this.srcにitemsMenu指定がある。B
menusrc = Utl.objectCopy( this.src.itemsMenu );
menusrc.items = this.selectedItem[ this.src.name ];
} else if( this.selectedItem[ "contextMenu_" + this.context ] ) {
//選択した項目はメニューではないが、
//this.selectedItem内にcontextMenu指定がある。C
menusrc = this.selectedItem[ "contextMenu_" + this.context ];
} else {
//メニューの終了
if( this.mode == "selectmode" )
this.endmark( this.selectedItem);
else if( this.mode == "executemode" ) {
this.execute();
} else {
alert( [ "undefined mode:", this.mode, "at menu.typeKey()" ].join( " " ) );
}
return;
}
//次のメニューを表示
let col;
if( menusrc.col == undefined )
col = this.style.col + this.selectedItem.name.length + 2
else
col = menusrc.col;
let row;
if( menusrc.row == undefined )
row = this.style.row + this.cursorY + 2;
else
row = menusrc.row;
let menu = new Menu( this.mode, this.context, menusrc, {
col : col,
row : row,
}, this.app );
this.appendChild( menu );
break;
case 66://B
if( this.endmark ) this.endmark( null );//ショップ等 ストーリー時
else this.parent.removeChild( this ); //キャンプ等 非ストーリー時
break;
default:
console.log( key );
}//switch
}//if else
}//typeKey()
// |
// V
async execute() {//m
let menus = new Array();
let element = this;
while( element instanceof Menu ) {
menus.unshift( element );
element = element.parent;
}
let script;
let argz = new Object();
for( let menu of menus ) {
if( menu.src.script ) script = menu.src.script;
argz[ menu.src.argzkey ] = menu.selectedItem;
console.log( menu.src.title, menu.src.argzkey );
}
//check.
if( ! script ) {
alert( "scriptが未定義の状態です。at menu.execute()" );
}
await script.call( this.app, argz );
//check. scriptによってサブメニューの元となったモノがなくなった。
//たとえば、道具:薬草>どうする:使う>だれに:キャラ1 で薬草を消費したら
//薬草由来の どうするメニュー、だれにメニュー は消さなければならない。
for( let i = menus.length - 1; i > 0; i -- ) {
let menu = menus[ i ];
let selfItem = menu.parent.selectedItem;
//親メニューに自身の選択項目がない。(薬草を使ったから薬草が消えた)
if( menu.parent.src.items.indexOf( selfItem ) == -1 ) {
menu.parent.removeChild( menu );
this.app.currentElement = menu.parent;
}
}
}
draw( cc ) {//m
super.draw( cc );
//ウィンドウとリストを描画
cc.save();
//原点を描画位置へ移動
let gx = this.style.col * this.style.cellSize;
let gy = this.style.row * this.style.cellSize;
cc.translate( gx, gy );
this.translateToItemArea( cc );
//カーソル
if( this.app.currentElement != this ||
this.app.metronomez.forCursor.toggle ) {
let w = this.style.fontSize * .7;
let h = this.style.fontSize * .8;
let gx = - this.style.fontSize + ( this.style.fontSize - w ) / 2;
let gy = this.cursorY * this.style.fontSize + ( this.style.fontSize - h ) / 2 + 1;
cc.beginPath();
cc.moveTo( gx, gy );
cc.lineTo( gx + w, gy + h / 2 );
cc.lineTo( gx, gy + h );
cc.closePath();
cc.fillStyle = "black";
cc.fill();
}
cc.restore();
super.drawChildren( cc );
}//draw()
}//Menu
class TitleScreen extends Element {//c
constructor( style, app ) {
super( style, app );
}
async typeKey( key ) {//m
this.app.artz.soundz.pushA.play();
this.app.removeChild( this );
this.app.mapScreen = new MapScreen( {
col : 0,
row : 0,
cols : this.app.style.cols + ( this.app.style.cols % 2 ? 0 : 1 ),
rows : this.app.style.rows + ( this.app.style.rows % 2 ? 0 : 1 ),
//偶数なら奇数にする
cellSize : 16,
}, this.app );
this.app.appendChild( this.app.mapScreen );
let map = this.app.status.player.position.map;
let x = this.app.status.player.position.x;
let y = this.app.status.player.position.y;
// this.app.mapScreen.playerMapMoveTo( map, x, y );
this.app.mapScreen.playerMapMoveTo( App.townmap, 3, 6 );
await this.app.tell( "スタート", { wait : true, close:true } );
}
draw( cc ) {//m
cc.fillStyle = "black";
cc.fillRect( 0, 0, cc.canvas.width, cc.canvas.height );
cc.fillStyle = "white";
cc.fillText( "hit any key", 100, 100 );
}
}//TitleScreen
// |
// V
class MapScreen extends Element {//c
constructor( style, app ) {
super( style, app );
this.actions = new Array();
this.colsHalf = Math.floor( this.style.cols / 2 );
this.rowsHalf = Math.floor( this.style.rows / 2 );
this.scroll = {
count : 0,
isAnimating : false,
tweakX : 0,
tweakY : 0,
}
this.keyLock = false;
}
// |
// V
tweakXForLooping( x ) {//m
//tweak. ループ時は範囲外の値を範囲内に調整する
if( this.cfg.maploop ) {
if( x < 0 )
x += this.mapWidth;
else if( x >= this.mapWidth )
x -= this.mapWidth;
}
return x;
}
tweakYForLooping( y ) {//m
//tweak. ループ時は範囲外の値を範囲内に調整する
if( this.cfg.maploop ) {
if( y < 0 )
y += this.mapHeight;
else if( y >= this.mapHeight )
y -= this.mapHeight;
}
return y;
}
getPositionAwayBy( step ) {//m
let dir = this.app.status.player.direction;
let x = this.sx + ( ( dir == 2 ) - ( dir == 0 ) ) * step;
let y = this.sy + ( ( dir == 3 ) - ( dir == 1 ) ) * step;
return { x : x, y : y }
}
// |
// V
typeKey( key ) {//m
if( key == 32 ) {
//check. 向いている方向に何かがある
let pos = this.getPositionAwayBy( 1 );
let bit;
if( bit = Utl.accessBy( this.mapBits, pos.y, pos.x ) ) {
if( bit == "□" ) {
pos = this.getPositionAwayBy( 2 );
this.actions.push( { x : pos.x, y : pos.y, type : "contact" } );
return;
}
}
if( bit = Utl.accessBy( this.storyBits, pos.y, pos.x ) ) {
if( bit.sprite ) {
this.actions.push( { x : pos.x, y : pos.y, type : "contact" } );
return;
}
}
//コマンドメニューを開く
this.app.artz.soundz.pushA.rapidPlay();
let campMenu = new Menu( "executemode", "camp", this.app.menusrcz.camp, {
col : 1,
row : 1,
}, this.app );
this.appendChild( campMenu );
}
}
// |
// V
senseKey( key ) {//m
//check.
if( this.scroll.isAnimating || this.keyLock ) return;
let addX, addY;
switch( key ) {
case 37: this.app.status.player.direction = 0; addX = -1; addY = 0; break;
case 38: this.app.status.player.direction = 1; addX = 0; addY = -1; break;
case 39: this.app.status.player.direction = 2; addX = 1; addY = 0; break;
case 40: this.app.status.player.direction = 3; addX = 0; addY = 1; break;
default: return;
}
//check. 移動先は壁
let sx = this.tweakXForLooping( this.sx + addX );
let sy = this.tweakYForLooping( this.sy + addY );
let bit;
if( bit = Utl.accessBy( this.mapBits, sy, sx ) )
if( this.cfg.walls.indexOf( bit ) > -1 ) {
this.app.artz.soundz.pushWall.play();
return;
}
if( Utl.accessBy( this.storyBits, sy, sx, "sprite" ) ) return;
//1マススクロールの起動
this.scroll.addX = addX;
this.scroll.addY = addY;
this.scroll.count = 0;
this.scroll.isAnimating = true;
}//senseKey
// |
// V
frameForScroll() {//m
//1マススクロールにおける、1ドット分のスクロール処理
this.scroll.tweakX -= this.scroll.addX;
this.scroll.tweakY -= this.scroll.addY;
//tweakX,Yが1ドットスクロールの主要のしくみ
//それについてはthis.draw()を参照してください。
this.scroll.count ++;
//check. スクロールの終了
if( this.scroll.count == this.style.cellSize ) {
this.scroll.isAnimating = false;
this.scroll.tweakX = 0;
this.scroll.tweakY = 0;
//draw()はスクロール終了後も参照するからここで0にする。
this.sx = this.tweakXForLooping( this.sx + this.scroll.addX );
this.sy = this.tweakYForLooping( this.sy + this.scroll.addY );
//マップ外に乗ったストーリー発生
if( ! this.cfg.maploop
&& ( this.sx < 0 || this.sx >= this.mapWidth
|| this.sy < 0 || this.sy >= this.mapHeight )
) {
this.actions.push( { x : -1, y : -1, type : "got_on" } );
} else {
//座標に乗ったストーリー発生
this.actions.push( { x : this.sx, y : this.sy, type : "got_on" } );
}
}//if スクロール終了
}//frameForScroll()
// |
// V
playerMapMoveTo( mapdatafunc /*other args*/ ) {//m
//マップ間移動
//check.
if( this.app.bgm ) {
this.app.bgm.pause();
this.app.bgm.currentTime = 0;
}
//データを記述した関数からデータを取り出す1
let commentData = mapdatafunc.toString().match( /\/\*\r\n([\s\S]+)\r\n\*\// )[ 1 ];
//データを記述した関数からデータを取り出す2
this.cfg = mapdatafunc();
//取り出したデータを使えるように加工
this.mapBits = new Array();
this.storyBits = new Array();
this.stories = new Object();
//マップデータを読み取る ここから
let storyTmp = new Array();
let lines = commentData.split( /\r\n/ );
for( let y = 0; y < lines.length; y++ ) {
let line = lines[ y ];
this.mapBits[ y ] = new Array();
for( let x = 0; x < line.length; x++ ) {
let mapBit = line.substr( x, 1 );
//check. 半角文字のときはストーリーへのショートカット
if( mapBit.match( /[0-9a-z ]/i ) ) {
let shortcutId = mapBit + line.substr( x + 1, 1 );
shortcutId = shortcutId.replace( / /, "" );
//check. ショートカットがリンク切れ
if( ! this.cfg.shortcuts[ shortcutId ] ) {
alert( "マップ上に配置したショートカットがリンク切れ id:" + shortcutId );
continue;
}
//マップ上のショートカットについては後で処理
storyTmp.push( {
shortcutId : shortcutId,
x : x,
y : y,
} );
//ショートカットをマップデータに置き換え
mapBit = this.cfg.shortcuts[ shortcutId ].bit;
x++; //1文字多く読んだから
}
this.mapBits[ y ].push( mapBit );
}
}//for
this.mapWidth = this.mapBits[ 0 ].length;
this.mapHeight = this.mapBits.length;
//storyBits初期化
for( let y = 0; y < this.mapHeight; y++ ) {
this.storyBits[ y ] = new Array();
for( let x = 0; x < this.mapWidth; x++ ) {
this.storyBits[ y ][ x ] = null;
}
}
//storyBitsにstoryを配置
for( let tmp of storyTmp ) {
let shortcut = this.cfg.shortcuts[ tmp.shortcutId ];
this.storyBits[ tmp.y ][ tmp.x ] = shortcut;
//tweak. 座標情報をセット
shortcut.x = tmp.x;
shortcut.y = tmp.y;
}
//マップデータを読み取る ここまで
//BGM
if( this.cfg.bgmTitle ) {
this.app.bgm = this.app.artz.bgms[ this.cfg.bgmTitle ];
this.app.bgm.loop = true;
this.app.bgm.play();
}
//マップの外に出たときのストーリー
if( ! this.cfg.maploop ) {
let story = this.cfg.shortcuts.outer.story.got_on;
this.stories[ "outer,got_on" ] = story;
}
//メソッドの引数によって処理を分ける(オーバーロードみたいに)
let argtypes;
let argArray = Array.from( arguments );
argtypes = argArray.map( arg => ( typeof arg ).substr( 0, 1 ) ).join( "" );
//ショートカットを指定して移動
if( argtypes.indexOf( "fs" ) == 0 ) {
//playerMapMoveTo( function, string [,number] )
let shortcut = arguments[ 1 ];
this.sx = this.cfg.shortcuts[ shortcut ].x;
this.sy = this.cfg.shortcuts[ shortcut ].y;
//check.
if( typeof arguments[ 2 ] !== "undefined" )
this.app.status.player.direction = arguments[ 2 ];
} else if( argtypes.indexOf( "fnn" ) == 0 ) {
//移動先位置の座標指定して移動
//playerMapMoveTo( function, number, number [,number] )
this.sx = arguments[ 1 ];
this.sy = arguments[ 2 ];
//check.
if( typeof arguments[ 3 ] !== "undefined" )
this.app.status.player.direction = arguments[ 3 ];
} else {
alert( "playerMapMoveTo()の引数リストが想定外 : " + argtypes );
}
}//playerMapMoveTo()
// |
// V
playerMapMoveTo_fx( mapdatafunc /*other args*/ ) {//m
//視覚効果付きのマップ間移動
let argArray = Array.from( arguments );
//check. 歩く音
let donotFootsteps = false;
if( ( typeof argArray[ argArray.length - 1 ] ) == "boolean" ) {
donotFootsteps = argArray.pop();
}
if( ! donotFootsteps ) this.app.artz.soundz.walk.play();
this.keyLock = true;
//クロージャ(関数が終わっても、この関数内で定義した関数内では生き続ける変数)
let countMax = 3;
let count = countMax;
let flg = true;
//マップ間移動時の画面フェードアウト、イン
let metronome = this.app.addMetronome( "forFade", 125 );
this.app.anms.push( function() {
//check. メトロノームに合わせて
if( ! metronome.changed ) return;
if( flg ) {
//フェードアウト
count --;
let value = 100 * count / countMax;
this.app.cc.canvas.style.filter = "brightness( " + value + "% ) ";
//check. フェードアウトの終了
if( ! count ) {
//マップ間移動する
this.playerMapMoveTo( ...argArray );
flg = false;
count = countMax;
}
} else {
//フェードイン
count --;
let value = 100 * ( countMax - count ) / countMax;
this.app.cc.canvas.style.filter = "brightness( " + value + "% ) ";
//check. フェードインの終了(全終了)
if( ! count ) {
this.keyLock = false;
delete this.app.metronomez.forFade;
return true; //trueはアニメの終了の意
}
}
}.bind( this ) );
}//playerMapMoveTo_fx()
searchStory() {//m
let story = null;
for( let action of this.actions ) {
//ストーリー マップの外に出た
if( action.type == "got_on" && action.x == -1 && action.y == -1 ) {
story = this.stories[ "outer,got_on" ];
break;
}
//ストーリー その他
if( story = Utl.accessBy( this.storyBits, action.y, action.x, "story", action.type ) ) {
break;
}
}
this.actions.length = 0;
return story;
}
draw( cc ) {//m
cc.fillStyle = "black";
cc.fillRect( this.x, this.y, this.w, this.h );
//this.sx,this.syが画面中央に来るように描画開始座標を調整
let sx = this.sx - this.colsHalf + this.mapWidth;
let sy = this.sy - this.rowsHalf + this.mapHeight;
//+ this.mapWidth,Heightは、下の計算で % this.mapWidth,Height
//としたときに負値になるのを避けるために加えた
cc.save();
cc.translate(
-this.style.cellSize / 2 + this.scroll.tweakX,
-this.style.cellSize / 2 + this.scroll.tweakY
);
//tweakX,Yが描画位置を1ドットずつずらすので
//きれいに1ドットスクロールしてるようにみえる
cc.font = this.style.cellSize + "px ''";
cc.fillStyle = "rgb(0,255,0)";
for( let row = 0; row < this.style.rows; row++ ) {
let y = ( sy + row ) % this.mapHeight; //端を超えると反対側に
let gy = row * this.style.cellSize;
for( let col = 0; col < this.style.cols; col++ ) {
let x = ( sx + col ) % this.mapWidth; //端を超えると反対側に
let bit = this.mapBits[ y ][ x ];
let gx = col * this.style.cellSize;
let image = this.cfg.images[ bit ] ? this.cfg.images[ bit ] : bit;
cc.fillText( image, gx, gy + this.style.cellSize );
}
}
//スプライト描画
for( let row = 0; row < this.style.rows; row++ ) {
let y = ( sy + row ) % this.mapHeight; //端を超えると反対側に
let gy = row * this.style.cellSize;
for( let col = 0; col < this.style.cols; col++ ) {
let x = ( sx + col ) % this.mapWidth; //端を超えると反対側に
let story = this.storyBits[ y ][ x ];
//check.
if( ! story ) continue;
if( ! story.sprite ) continue;
let gx = col * this.style.cellSize;
let image = story.sprite;
cc.fillText( image, gx, gy + this.style.cellSize );
}
}
//debug. グリッド
if( 0 ) {
cc.strokeStyle = "rgba(255,255,255,.125)";
for( let row = 0; row < this.style.rows; row ++ ) {
for( let col = 0; col < this.style.cols; col ++ ) {
let x = col * this.style.cellSize;
let y = row * this.style.cellSize;
cc.strokeRect( x, y, this.style.cellSize, this.style.cellSize );
}
}
}
cc.restore();
//debug. 画面の中央線
if( 1 ) {
cc.strokeStyle = "red";
cc.beginPath();
cc.moveTo( 0, cc.canvas.height / 2 );
cc.lineTo( cc.canvas.width, cc.canvas.height / 2 );
cc.stroke();
cc.beginPath();
cc.moveTo( cc.canvas.width / 2, 0 );
cc.lineTo( cc.canvas.width / 2, cc.canvas.height );
cc.stroke();
}
//プレイヤーキャラ描画
cc.fillStyle = "cyan";
cc.font = this.style.cellSize + "px ''";
let gx = ( cc.canvas.width - this.style.cellSize ) / 2;
let gy = ( cc.canvas.height - this.style.cellSize ) / 2 + this.style.cellSize;
let sprite = this.app.artz.spritez.player;
let idx = this.app.status.player.direction * 2 + sprite.metronome.index;
let image = sprite.images[ idx ];
cc.fillText( image, gx, gy );
//子の描画(ウィンドウなど)
for( let child of this.childs ) child.draw( cc );
for( let float of this.floats ) float.draw( cc );
}//draw()
}//MapScreen
App.worldmap = function() {/*
●〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃¥¥¥▲〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
¥山山山山山〃~~~~~~¥¥〃〃〃〃〃〃〃〃〃¥¥山山山¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
¥山山山〃〃〃〃~~~〃〃〃〃〃〃〃〃〃〃〃〃¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
¥山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
¥¥〃〃1 〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃〃〃山山〃〃〃〃〃〃〃¥¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃山山山〃〃〃〃〃〃〃¥¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃山山山山山〃〃〃〃〃〃〃〃¥¥山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃山山山〃〃〃〃〃〃〃〃¥〃〃¥山山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥¥〃〃〃〃山山山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥〃〃〃〃〃山山山山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
山山山〃〃〃~~~〃〃〃〃〃〃〃〃〃〃山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
山山〃〃~~~~¥¥〃〃〃〃〃〃〃〃山山山山〃〃〃〃〃〃〃山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
山〃〃~~~¥¥¥〃〃〃〃〃〃〃〃山山山山山山山〃〃〃〃〃山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃¥¥¥¥〃〃〃〃〃〃〃〃〃山山山山山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃¥¥¥〃〃〃〃〃〃〃山山山山山山山山山山~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃¥¥¥〃〃〃〃〃〃山山山山山~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃¥¥¥〃〃〃〃〃〃〃〃〃〃~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃¥¥¥¥¥¥¥〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃¥¥¥¥¥¥¥¥〃〃〃〃〃〃〃〃~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃¥¥¥¥¥¥〃〃〃〃〃〃〃〃〃~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃¥¥¥¥¥〃〃〃〃〃〃〃〃〃〃~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃¥¥〃〃〃〃〃〃〃〃〃〃~~~~~〃〃〃〃〃〃〃¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
*/
return {
maploop : true,
bgmTitle : "field",
walls : "~",
shortcuts : {
"1" : {
bit : "凸",
story : {
"got_on" : {
begin : function( e ) {
this.mapScreen.playerMapMoveTo_fx( App.townmap, "s", 2 );
},
},
},
},
},
images : {
"¥" : "🌲",
"凸" : "🏰",
"山" : "⛰",
"~" : "🌊",
},
}//return
}//worldmap
App.townmap = function() {/*
木木木木木木木〃・・〃木木木木木木木木木〃・・〃〃〃〃〃〃〃
木■■■■■木〃・・〃木■■■■■■■木〃・・〃〃〃〃〃〃〃
木■刀刀刀■木〃・・〃木■■■■■■■木〃・・〃〃〃〃〃〃〃
木■・ws・■木〃・・〃木■■■■■■■木〃・・〃〃〃〃〃〃〃
木■■□■■木〃・・〃木■■■扉■■■木〃・・〃〃〃〃〃〃〃
木木〃・剣木木〃・・〃〃花花花・花花花〃〃・・〃〃〃〃〃〃〃
s ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・
・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・
木木盾・〃木木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
木■■□■■木〃・・〃a1〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
木■・ss・■木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
木■盾盾盾■木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
木■■■■■木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
木木木木木木木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
*/
return {
maploop : false,
bgmTitle : "town",
walls : "■□扉",
shortcuts : {
outer : {
bit : "〃",
story : {
"got_on" : {
begin : function( e ) {
this.mapScreen.playerMapMoveTo_fx( App.worldmap, "1", 3 );
},
},
},
},
"s" : {
bit : "・",
},
"ws" : {
bit : "👨🔧",
story : {
"contact" : {
begin : async function( e ) {
await this.tell( "ここは刀剣屋だ。\n" );
await this.shop();
}
},
},
},
"a1" : {
bit : "〃",
sprite : "🕵️♂️",
story : {
"contact" : {
begin : async function( e ) {
await this.tell( "ドラゴンなら北のほうへ飛んで行ったぜ", { wait : true, close:true } );
}
},
},
},
"ss" : {
bit : "👨🔧",
},
},
images : {
"木" : "🌳",
"花" : "🌷",
"扉" : "🚪",
"箱" : "📦",
"座" : "🪑",
"寝" : "🛏",
"∪" : "🏺",
"剣" : "⚔",
"刀" : "🗡",
"盾" : "🛡",
},
}//return
}//townmap
現在コマンドメニューのプログラミングに時間がかかっています。
コマンドメニューと一言で言っても、
のようにいろいろな場面で使える、ザ・ウィンドウシステムになっています。(母音の前だからただしくはジ・ウィンドウシステム)
万能型で、なおかつ、簡潔に書きたいんですが、いまのところ一部が複雑なしくみになっていて、それをどのように簡潔にするのかが、ひとつの課題になっています。
現在はオブジェクト指向(クラスやクラス同士の継承)を使っていますが、将来的には BASIC でも実装できるようなプログラムにしたいと思っています。
(プログラムの著作権についてはあまり深く考えていませんが、もし使いたい場合は、自由に使ってください。試作品なので使えないは思いますが)
個人的なことですが、私の子供のころからあこがれていたプログラムは
の3点だったかなと思います。
このうち、1番の「スムーズスクロール」は、今回の試作品で実現されています。最近のパソコンとブラウザの処理スピード向上の恩恵かなと思います。あと JavaScript の requestAnimationFrame() を使用して vsync(ハードウェア的な描画信号)にあわせて描画することができるようになったことも大きいでしょう。とてもうれしいことです。
2番の「BGM を鳴らす」は今回の試作品でも鳴っていますが実は満足していません。音源が mp3 や wav なので、どうしても曲データの終わりで一度音が止まり最初から繰り返すという鳴り方になってしまいます。できれば楽譜のレベルでループで繰り返してほしいんです。この辺は同じ BGM をプログラム中に2つ用意して、2つの BGM をクロスフェードで重ねればある程度不満が軽減されるかなと思っています。または FM 音源の自作みたいなことも 難しいとは思いますが考えています。
3番の 3DCG は h = x * s / z、v = y * s / z の2式で実現できるので、満足しています。(今回の試作とは関係ありません)
今後は、戦闘システムを作ることになると思います。
(訪問者のどんなニーズと この記事がつながるか)
4連休 2021年7月22日(木)~2021年7月25日(日)
お盆休み 2021年8月7日(土)~2021年8月16日(火) 10日間
決めごと:
上記アンダーライン部分が普通じゃない記述です。その説明を…
最近、J.R.R.トールキンの「指輪物語」を読んでいて、なんとなく挿絵がほしくなって自分で描いて本に はさんでいます。
▼J.R.R.トールキンの「指輪物語」 トールキンはノーベル賞候補だった |
▼P97付近 異変のうわさ |
▼P101付近 ガンダルフ2度目の登場 |
▼P102 そしてフロドと指輪の脅威について話す |
これを18時以降に可とします。でも1枚も描かないかもしれません。
ちなみに、この指輪物語は全部で10巻あり、amazon にて中古品で購入しました。
現在、1巻の 2/3 程度まで読んだところですが、ノーベル賞候補だったというのがよくわかります。↓
ただし、人によっては苦痛となりうるところもあるかも。↓
しかし、Wikipedia で調べてみるとその翻訳は、
訳 瀬田貞二 氏(1916年生まれ)
同氏は 1941 年に 東京 帝国大学 国文科 を卒業し、中学教師のかたわら児童文学作品を雑誌に寄稿し、1949年(訳者33才)に児童文学関係の出版社に入社、その後26年ほど児童文学の仕事にたずさわってきた。そして、
指輪物語の翻訳 1975年 訳者59才
ということなので、つまり、私が生まれて間もない1歳のときに、その道で活躍してきた60才くらいのご年配の方が洋書の翻訳をされたわけで、どこか日本寄りの古臭い表現になるのは、当然と言えば当然かもしれません。私が生まれて小学生くらいのころまで郵便ポストは赤い円筒形の陶器製だったし、自動販売機ではガラス瓶の1.5リットル コカ・コーラ(重い)が売られており、昔というのは確かに文化が違います。。鹿島立ち、昔の表現だからと言って悪く言うこともないでしょう。。
または、より正しい意味合いを、日本の人々に伝えるためには、日本の人々が親しんでいる言葉を使うのがベストだ、、という方針があったのかもしれません。
ともあれ、RPGのただ一点の祖先がD&Dであり、D&Dが作られるために必要だった「剣と魔法のファンタジー」という文化の上流にこの「指輪物語」があります。「指輪物語」を読むことで、RPG のエッセンスを垣間見ることができると思って読んでいます。
(私の「普通じゃない記述」について説明しているところでした)
毎日決めて散歩していると、あきてきて、意味がないような気がするんです。
でもずっと部屋にいるよりは確かに身体には良いです。
なので、良いと思った、思わなかった、に関係なく散歩に毎日出かけたという結果がほしいと思って、毎日「良いと思わなかった」と記録を取ろう、と考えたわけです。あの手この手で自分をそっち方向へ向かわせたいんです。
もちろん良いに越したことはなく、良いと思ったら「良いと思った」と記録します。
4連休:
7月22日(木) | 良いと思った |
7月23日(金) | 良いと思った |
7月24日(土) | 良いと思った |
7月25日(日) | 良いと思った |
お盆休み:
8月7日(土) | |
8月8日(日) | |
8月9日(月) | |
8月10日(火) | |
8月11日(水) | |
8月12日(木) | |
8月13日(金) | |
8月14日(土) | |
8月15日(日) | |
8月16日(月) |
なお、いつもなら勉強を予定するところですが、今回は今のところ予定せず、RPG開発 をなんとかしてひと段落させたいと考えています。
ページの最初の「NO PC WEEK に代わる PC 使用制限のしくみ(新β版)」の表で、下図のようにコマンドメニュー作成のパーセンテージを 0.1% ずつ微妙に進めている理由は、現在、プログラムを根本的に書き直しているからです。
これら3種類の要素について、それぞれ別々にプログラムを書くのは普通の考え方ですが、それだとどうもプログラムが煩雑になって、行き詰ってしまうので、3種類の要素を1つの考え方で統一(3種類とも1つのクラスを継承)するように書き直しています。
(訪問者のどんなニーズと この記事がつながるか)
今月は「よせてはかえす波」を文字のカタでくりぬいて表示、というスクリプトです。
前々から「文字そのものにゆらゆらとした波のアニメが表示される」、、というイメージが頭にあって、できそうな気がしてやってみました。
一応できましたが、ちょっと無理な方法で実現させたので、もしかしたら表示するブラウザ、パソコン、スマートホンによってはうまく表示できないこともあるかもしれません。
しくみ
これだけならまだ良かったんですが、Firefox で描画内容がキャッシュされるまでのあいだ、文字部分がチラチラとちらつくので、事前にアニメーションのすべてのパターンを Image オブジェクトにして配列に格納しておくようにします。
その格納は無制限に格納するわけには やはりいかない(メモリーには限りがある)ので、行うアニメーションはコマ数を限定します。
アニメーションの動きを自由に値を加算して気ままに行うのではなく、アニメの開始から終了までの動きの量(回転の角度とか、物体の座標とか)を、コマ数で割って、、というような考え方になります。そうなると面倒くさいですよね。、、でもちょっと説明してみましょう。
donbura = 6.28 / this.counterMax * this.counter;
たとえば、物の回転は皆さんご存知の通り、0度から360度で一回りです。
プログラミングでは360度は、6.28 という数値を使って計算するので、6.28 で一回りです。(ちなみに6.28は円周率3.14の2倍です)
これは、その米(ごはん)が 1合なのか 180cc なのかという単位の違いと同じことなので、そんなに難しい話ではないはずです。
0.5合は 90cc という計算ですよね。同じことをここでもやります。
アニメで物を1回転させたいとき、6.28 が動きの全体量。そしてコマ数は限られていてその数は this.counterMax という変数に入っています。10回や100回などそういう数値が入っています。
全体量 6.28 を this.counterMax で割れば、「1コマ当たりの移動量」がわかります。
this.counterMax が 10 回ならば、6.28 / 10 で、「1コマ当たりの移動量」は 0.628 です。0.628 ずつ 10 回繰り返せば、6.28 になり、一回転となるわけです。
それでアニメが現在どのくらいまで進んだのかは、this.counter でカウントしていて、「1コマ当たりの移動量」に this.counter を掛け算すれば、現在のコマ数での回転の量がわかるわけです。
その回転の量を使って、何かを描けばそのコマを描くことができます。
気ままなアニメとは考え方が違うので、少し難しいかもしれませんね。
そういう感じで Firefox のためにアニメの考え方を変えて、対応したわけです。。が、
「Firefox はキャッシュを意図的に作ってあげれば ちらつく不具合が解消される」とは、誰も言っていなくて、私がそれで解決するだろうと勝手に思っただけです。
そのため、見ていると、Firefox は何かのタイミングでアニメのキャッシュ(そもそもそんなものがあるのだろうか??)を開放するらしく、プログラムのほうでアニメのコマ画像をいくら保持していても(Firefoxが開放してしまった際には)そんなの関係ないみたいです。ときどきちらつきます。
でも、
そんなのかんけーねぇ! そんなのかんけーねぇ! |
プログラムは途中で疲れてしまい、はやく完成させようと思って、殴り書きみたいなプログラムになってしまいました。
このページのどこでも白い部分を右クリックして、「ソースコードを表示」を選び(表示されるまで時間がかかるかも)、文字列検索で「wave」を検索すれば、当該のプログラムが出てきます。
プログラムを見ていくと、途中で class AnimationBGI {...} というクラス定義が出てきます。これはHTML要素の背景にCANVASの内容を適用するためのツールみたいなものです。が、今後使うかどうかはわかりません。
(訪問者のどんなニーズと この記事がつながるか)
ホームページ上で JavaScript のゲームを遊んだり、面白い動きをする JavaScript プログラムを見るとき、画面の表示が小さいとつまらないですよね?
以前、私は WebBasic と称して、自動的に適切な大きさに拡大してくれる便利プログラムを JavaScript で自作していました。
スクロールしてプログラムの画面がウィンドウの外に出ると、プログラムの動きを止めて、CPUパワーを節約するという機能も搭載していました。
とても便利な感じがしていたんですが…
今日になって それらの便利な機能は、JavaScript の基本機能で実現できることがわかりました。(気づくのがおそい)
以下のプログラムは、ブラウザのウィンドウの大きさに合わせてページにうまくフィットする(横幅の65%の大きさにする)ように大きさが拡大され、ページをスクロールして画面が見えなくなると動きが止まります。
その JavaScript の基本機能を使ったプログラムはこんな感じです↓
拡大機能に関する部分は /*1*/、 スクロールで停止する機能に関する部分は/*2*/、
1つのページ内で複数のプログラムを掲載し動かす機能については /*3*/ で示しています。
拡大機能は、CSSの object-fit プロパティで実現し、スクロールで停止の機能は、JavaScript の IntersectionObserver で実現しています。
複数掲載については、個々のプログラムをそれぞれ class か、オブジェクトのいずれかにして、そのなかで完結するようにプログラムを作成します。それらをページ中の appz という配列(appz.app1のように名前で参照できるObject型の配列)におさめ、まぁ、上手に管理します。このアバウトな言い方が初心者の方には苦しいんだと思いますが、下のプログラムを参考にしてください。(時間がないので)
この青い部分をダブルクリックするたびに、リストの全体を表示/非表示します。
タッチデバイスの場合はタッチします。
<div style="text-align:center;">
<canvas id="test012" width="256" height="224" style="
height : 80vh; /*1*/
image-rendering : crisp-edges;
object-fit : contain; /*1*/
background-color : gray;
border : solid 8px gray;
border-radius : 16px;
"></canvas>
</div><p>
<script>
class Test012 {
constructor() {
this.name = "test012";
this.cc = document.getElementById( this.name ).getContext( "2d", { alpha : false } );
this.t = 0;
}
frame() {
this.draw( this.cc );
this.t += 0.1;
this.timerId = requestAnimationFrame( this.frame.bind( this ) );
}
draw( cc ) {
cc.clearRect( 0, 0, cc.canvas.width, cc.canvas.height );
let r = Math.min( cc.canvas.width / 2, cc.canvas.height / 2 );
cc.beginPath();
cc.arc( cc.canvas.width / 2, cc.canvas.height / 2, r, this.t, this.t + 5 );
cc.strokeStyle = "#0F0";
cc.stroke();
cc.beginPath();
cc.arc( cc.canvas.width / 2, cc.canvas.height / 2, r - 10, -this.t - 1, -this.t - 5 );
cc.strokeStyle = "#F00";
cc.stroke();
cc.beginPath();
cc.arc( cc.canvas.width / 2, cc.canvas.height / 2, r - 20, this.t + 2, this.t + 5 );
cc.strokeStyle = "#00F";
cc.stroke();
}
start() { /*2*/
this.frame(); /*2*/
} /*2*/
stop() { /*2*/
cancelAnimationFrame( this.timerId ); /*2*/
this.timerId = null; /*2*/
} /*2*/
}
addEventListener( "load", function() { /*1, 2, 3*/
//check.
if( typeof appz === "undefined" ) { /*3*/
appz = new Object(); /*3*/
}
//check. 要素がユーザーの目に入ったかどうか /*2*/
if( typeof intersectionObserver === "undefined" ) { /*2*/
intersectionObserver = new IntersectionObserver( function( entries ) { /*2*/
for( let entry of entries ) { /*2*/
let app = appz[ entry.target.id ]; /*2*/
let bak = app.visible; /*2*/
app.visible = entry.intersectionRatio > 0; /*2*/
//check. /*2*/
if( bak == app.visible ) continue; /*2*/
if( app.visible ) { /*2*/
console.log( "app.start" ); /*2*/
app.start(); /*2*/
} else { /*2*/
console.log( "app.stop" ); /*2*/
app.stop(); /*2*/
} /*2*/
} /*2*/
}, { rootMargin : '-40% 0% -40% 0%', } ); /*2*/
} /*2*/
appz.test012 = new Test012(); /*3*/
intersectionObserver.observe( appz.test012.cc.canvas ); /*2*/
} );
</script>
作った WebBasic が水の泡!
骨折り損のくたびれ儲け とは まさにこのことだ フガッフガッ |
↑いちおう、このキャラには「美重在 圭」(びじゅある けい)という名前がついています。
複数のプログラムが動くという証明として、もう1つプログラムを掲載します↓
上のプログラムリストは HTML の一部の抜粋でしたが、以下のプログラムリストは HTML の全体です。この青い部分をダブルクリックするたびに、リストの全体を表示/非表示します。
タッチデバイスの場合はタッチします。
このHTMLファイルと、使用している画像のリンクをそれぞれ右クリックして「名前を付けてリンク先を保存」等してダウンロードし、一緒のフォルダに入れて、HTMLファイルをインターネットブラウザで開けばお手元で動作すると思います。JavaScript プログラミングの勉強に役立ててください。
(訪問者のどんなニーズと この記事がつながるか)
2年前くらいから、このホームページ上で私自身のパソコンの使用時間について、「NO PC WEEK に代わる PC 使用制限のしくみ(新β版)」という表を使って管理し、パソコンの長時間の作業で身体を壊さないようにしています。
その追加の新しい仕組みとして「SUPER PC WEEK」というものを考えました。
これはパソコン使用時間の制限ではなく、「制限を無し」にして「好きなだけパソコンを使って良い」とするものです。
「NO PC WEEK」はそこそこうまくいっていて効果が出ています。その「NO PC WEEK」の一部の仕組みを使えば「好きなだけパソコンを使って良い」としても問題なさそうだと考えました。
また、連休中に行うものなので、仕事への影響も少ないと思います。
詳しくは、「NO PC WEEK に代わる PC 使用制限のしくみ(新β版)」の表の下部にあるタブの並びの左から3番目「例外事項」タブを見てください。
今日から3連休なので、「SUPER PC WEEK」にします。
おもに「全体的・基本・簡潔のRPG」の作業を行うつもりです。
(電子回路とか、電子機器組み立てとか、おざなりになっているのが少し気がかりですが)
(訪問者のどんなニーズと この記事がつながるか)
Shade3D は形状を直接ぼやぁっと光らせる機能がなく、「ソフトグロー」という材質設定を行った「別の物体」を重ねることで疑似的に光らせることができます。下図
グリフォンの翼で直接いろいろ試すのも良いんですが、レンダリングに時間がかかるので、この立方体のような単純な形で試作するのが良いです。
絵画を描く人も、プログラミングをする人も、モデリングをする人も、本番作品でいろいろ試さず、別途試作を作るのが効率がいいみたいです。
なお、Blenderだと簡単に光らせられるみたいです。これ(リンクはBingで画像検索)
試作と同じことをグリフォンでやろうとして、うまくいかず、今日は時間切れ。↓
ちょっと大変そうだけど、ちゃんとできるようになれば、表現力が上がるから頑張って行うことにします。
赤で発光させれば「燃えさかる」という表現もできそうです。
…できれば目玉の黒目も動かしたい…それもできれば表情豊かになるから。
自作RPGのスクリーンショットとプログラムを掲載していますが、動くものへのリンクはこの記事の中にありません。
コマンドメニュー部分のプログラミング 95 %の状態です。
前回からの変更点:
以下はその自作RPGのプログラムリストの全体です。(1520行あります)
この青い部分をダブルクリックするたびに、リストの全体を表示/非表示します。
タッチデバイスの場合はタッチします。
/* memo.
名前のすみわけ
プログラム 物語 対策
----------------------------
イベント イベント 物語のイベント(シナリオスクリプト)をストーリーと呼ぶ
アイテム アイテム 物語のアイテム(どうぐ)をトレジャーと呼ぶ
その他の名前
配列変数の名前と、同じ内容の連想配列変数の名前について
たとえばキャラクターの配列と、同じキャラクターの連想配列の名前について
どちらも複数形の名前にするとcharactersになるので、
配列時 characters
連想配列時 characterz
とする。
spritesの読み方について
配列時 sprites スプライトス
連想配列時 spritez スプライトス
万一、プログラムについて話し合うときに「このスプライト ズ だけど…」というと
不自然なので、どちらもスプライト ス と読むことにする。
*/
class App {//c
constructor( canvas ) {
this.name = "app";
this.cc = canvas.getContext( "2d", { alpha : false } );
this.cc.canvas.width = 256;
this.cc.canvas.height = 224;
this.cellSize = 16;
//---メトロノーム
this.metronomez = new Object();
this.addMetronome( "t1000_i2" );
this.addMetronome( "t500_i2" );
this.addMetronome( "t250_i2" );
//カーソル専用(手動追加)
this.metronomez.forCursor = {
maxTime : 500,
maxIndex : 0,
time : 0,
index : 0,
toggle : true,
changed : false,
}
/*
メトロノームの各フラグの変化タイミング図解(タイミングチャート)
maxTime : 250,
maxIndex : 3, の場合の各フラグの動き↓
(経過時間) 0 ... 250 ... 500 ... 750 ... 1000 ...(ms)
time : 0, 0 ..249 0 ..249 0 ..249 0 ..249 0 .. ...(ms)
index : 0, 000000001111111122222222000000001111 ...
toggle : true, ~~~~~~~~________~~~~~~~~________~~~~ ...
changed : false, ________~_______~_______~_______~___ ...
~ : true
_ : false
... : 略
※このグラフ中の1文字単位の縦1列はrequestAnimationFrameに相当する
つまり、requestAnimationFrameごとに、
timeは経過時間を増やしていきmaxTimeになると0に戻る
indexはtimeがmaxTimeになるごとに1増え、maxIndexになると0に戻る
toggleはtimeがmaxTimeになるごとに真偽を反転する
changedはtimeがmaxTimeになったときだけ真になる
maxTimeでアニメのパラパラのスピード決め
toggleやchangedでアニメのパラパラの実行を判断
indexでアニメのパラパラの画の切り替え
*/
this.spritez = {
player : {
images : [ "←", "-", "↑", "|", "→", "-", "↓", "|" ],
metronome : this.metronomez.t500_i2,
},
}
this.anms = new Array();
this.artz = { //ex. resources
spritez : this.spritez,
bgms : new Object(),
soundz : new Object(),
}
this.screenz = {
title : new TitleScreen( this ),
map : new MapScreen( this ),
cmd : new CmdScreen( this ),
}
//各画面について
for( let name in this.screenz ) {
let screen = this.screenz[ name ];
//ショートカット
this[ name ] = screen;
}
this.characters = new Array();
this.characterz = new Object();
this.treasurez = new Object();
//---トレジャー
this.treasurez = {
"どうけん" : {
},
"せいどうけん" : {
},
"てっけん" : {
},
"こうてつけん" : {
},
"なんこう" : {
nextMenu : {
title : "どうする?",
items : [
{
name : "つかう",
title : "だれに?",
items : this.characters,
script : async function( cmd, char1, dougu, cmd2, char2 ) {
await this.tell( char1.name + "は" + char2.name + "に" + dougu.name + "を使った!", { close : true } );
let idx = char1[ cmd.name ].indexOf( dougu );
char1[ cmd.name ].splice( idx, 1 );
},
},
{
name : "わたす",
title : "だれに?",
items : this.characters,
script : async function( cmd, char1, dougu, cmd2, char2 ) {
await this.tell( char1.name + "は" + char2.name + "に" + dougu.name + "を渡した!", { close : true } );
},
},
{
name : "すてる",
script : async function( cmd, char1, dougu, cmd2 ) {
await this.tell( char1.name + "は" + dougu.name + "を捨てた!", { close : true } );
},
},
],//items
},//nextMenu
},//なんこう
}//treasurez
//tweak. キーを名前として要素内に登録
for( let key in this.treasurez ) {
this.treasurez[ key ].name = key;
}
//---キャラクター
this.characters.push( {
name : "キャラ1",
"どうぐ" : [
{ name : "つるぎ" },
this.treasurez[ "なんこう" ],
{ name : "つるぎ" },
],
} );
this.characters.push( {
name : "キャラ2",
"どうぐ" : [
{ name : "なんこう2" },
{ name : "つるぎ" },
],
} );
this.characters.push( {
name : "キャラ3",
"どうぐ" : [
{ name : "なんこう3" },
{ name : "つるぎ" },
],
} );
Utl.array2object( this.characters, this.characterz, element => element.name );
//---メニュー
this.menusrcs = [
{
name : "yesno",
title : "",
items : [
{
name : "はい",
},
{
name : "いいえ",
}
],
},
{
name : "camp",
title : "コマンド?",
items : [
{
name : "どうぐ",
title : "だれの?",
items : this.characters,
nextMenu : {
title : "どれ?",
row : 3,
},
},
{
name : "しらべる",
},
{
name : "まほう",
title : "だれの?",
items : this.characters,
},
],
},
]
this.menusrcz = Utl.objectFrom( this.menusrcs, element => element.name );
//素材のプリロード
return Promise.all( [
this.load( "bgm", "field", "_bgm/10 イルバーンズの遺跡 (遺跡・地上).wav" ),
this.load( "bgm", "town", "_bgm/1-05 街.wav" ),
this.load( "bgm", "battle", "_bgm/battle.wav" ),
this.load( "sound", "pushA", "_sound/pushA.mp3" ),
this.load( "sound", "pushWall", "_sound/pushWall.mp3" ),
this.load( "sound", "walk", "_sound/walk.wav" ),
] ).then( function() {
return this; //ここはコンストラクタなのでAppのインスタンスを返すこと
}.bind( this ) );
}//constructor()
addMetronome( name /*other args*/ ) {//m
//オーバーロード(メソッド名が同じで引数のパターンが異なる)の模造
//”関数の先頭で引数の型を判定する条件分岐で対応”
let argsId = "";
for( let arg of Array.from( arguments ) ) {
argsId += "," + ( typeof arg ).substr( 0, 1 );
}
let metronome;
switch( argsId ) {
case ",s" : //フォーマット名称 t時間_iインデックス上限
let tokens = name.split( /_/ );
metronome = {
maxTime : Number( tokens[ 0 ].substr( 1 ) ),
maxIndex : Number( tokens[ 1 ].substr( 1 ) ),
time : 0,
index : 0,
toggle : false,
changed : false,
}
this.metronomez[ name ] = metronome;
break;
case ",s,n" : //自由な名称、時間
let tm = arguments[ 1 ];
metronome = {
maxTime : tm,
maxIndex : 0,
time : 0,
index : 0,
toggle : false,
changed : false,
}
this.metronomez[ name ] = metronome;
break;
default:
alert( "addMetronome()にて、未定義の引数リスト:[" + args + "]" );
}
return metronome;
}//addMetronome()
async tell( message, flgs ) {//a
/* ストーリの記述で、いきなりmessageWindow.write()を行いたいとき、
messageWindowを未作成のとき、どうするか
という問題があるので、このtellメソッドは必要。
*/
//check. メッセージウィンドウがないときは作成する
if( ! this.messageWindow ) {
this.cmd.addElement( new MessageWindow( this ) );
}
//check. コマンド画面を有効にする
if( this.currentScreen != this.cmd ) {
this.currentScreen = this.cmd;
this.cmd.style.visibility = true;
}
await this.messageWindow.write( ...arguments );
}
async yesno( message, closeFlg ) {//m
await this.tell( message, { nokey : true } );
this.artz.soundz.pushA.rapidPlay();
this.cmd.style.visibility = true;
this.currentScreen = this.cmd;
let yesnoMenu = new Menu( this.menusrcz.yesno, 27, 24, this );
this.cmd.addElement( yesnoMenu, this.messageWindow );
return new Promise( function( endmark ) {
yesnoMenu.endmark = endmark;
}.bind( this ) ).then( function( selectedItem ) {
this.cmd.closeElement( yesnoMenu );
console.log( selectedItem );
if( selectedItem )
return selectedItem.name == "はい";
else
return false;
}.bind( this ) );
}
async shop() {//m
this.cmd.style.visibility = true;
this.currentScreen = this.cmd;
let menu = new Menu( {
name : "shop",
title : "刀剣屋 'ロバートウッドテイル'",
items : [
this.treasurez[ "どうけん" ],
this.treasurez[ "せいどうけん" ],
this.treasurez[ "てっけん" ],
this.treasurez[ "こうてつけん" ],
],
}, 2, 2, this );
this.cmd.addElement( menu );
while( 1 ) {
await this.tell( "何を買うんだ?\n", { nokey : true } );
let selectedItem = await new Promise( function( endmark ) {
menu.endmark = endmark;
}.bind( this ) );
//check.
if( ! selectedItem ) {
this.cmd.closeElement( menu );
break;
}
if( await this.yesno( selectedItem.name + "だな。\n" + "100エンだ。\n買うかね?\n" ) ) {
await this.tell( "まいどあり!\n\n", { nokey : true } );
}
this.cmd.currentElement = menu;
}
await this.tell( "ありがとうございました!", { close:true } );
this.currentScreen = this.map;
}
//プリロード
load( type, name, src ) {//m
return new Promise( function( endmark ) {
let object;
console.log( type, name, "loading.." );
switch( type ) {
case "bgm":
object = new Audio();
object.oncanplaythrough = function() {
this.artz.bgms[ name ] = object;
object.oncanplaythrough = null;
console.log( "\t", type, name, "loaded." );
endmark();
}.bind( this );
object.src = src;
object.load();
break;
case "sound":
object = new Audio();
object.oncanplaythrough = function() {
this.artz.soundz[ name ] = object;
object.oncanplaythrough = null;
console.log( "\t", type, name, "loaded." );
object.rapidPlay = function() {
object.pause();
object.currentTime = 0;
object.play();
/*
同じ音楽ファイルの連続演奏時は
最初にリセットをかける必要がある。
rapid=急速
*/
}
endmark();
}.bind( this );
object.src = src;
object.load();
break;
}
}.bind( this ) );
}//load()
async start() {//m
this.keys = new Array();
onkeydown = function( e ) {
if( this.keys.indexOf( e.which ) == -1 ) {
this.keys.push( e.which ); //キーセンス オン
this.currentScreen.typeKey( e.which, this );
//キータイプ 実行
}
}.bind( this );
onkeyup = function( e ) {
let idx = this.keys.indexOf( e.which );
if( idx > -1 ) this.keys[ idx ] *= -1;
//キーセンス オフ予定にする(同じ数の負値)
}.bind( this );
this.status = {
player : {
position : {
map : App.worldmap,
x : 0,
y : 0,
},
direction : 0,
},
}
//タイトル画面から始める
this.screenz.title.style.visibility = true;
this.currentScreen = this.screenz.title;
this.beforeTime = 0;
this.frame( 0 );
}//start()
frame( tm ) {//m
//経過時間
let diff = tm - this.beforeTime;
this.beforeTime = tm;
//メトロノーム書き換え
for( let key in this.metronomez ) {
let metronome = this.metronomez[ key ];
metronome.time += diff
//check. そのメトロノームは時を刻んだ
if( metronome.time >= metronome.maxTime ) {
metronome.toggle = ! metronome.toggle;
metronome.changed = true;
metronome.time = 0;
//check. インデックスを進める
metronome.index++;
if( metronome.index == metronome.maxIndex )
metronome.index = 0;
} else {
metronome.changed = false;
}
}
//キーセンス 処理
for( let i = this.keys.length - 1; i >= 0; i-- ) {
let key = this.keys[ i ];
this.currentScreen.senseKey( Math.abs( key ), this );
//キーセンス 実行
//check. キーセンスオフ予定ならキーセンス オフ
if( key < 0 ) this.keys.splice( i, 1 );
}
//1ドットスクロール実行
if( this.screenz.map.scroll.isAnimating ) {
this.screenz.map.frameForScroll();
}
//ストーリー検索&実行
let story;
if( story = this.currentScreen.searchStory() ) {
story.begin.call( this );
}
//アニメ処理
for( let i = this.anms.length - 1; i >= 0; i-- ) {
if( this.anms[ i ]() ) this.anms.splice( i, 1 );
}
this.draw( this.cc );
this.timerId = requestAnimationFrame( this.frame.bind( this ) );
}//frame()
draw( cc ) {//m
cc.clearRect( 0,0,cc.canvas.width, cc.canvas.height );
for( let screen of Object.values( this.screenz ) ) {
if( screen.style.visibility ) screen.draw( cc );
}
}
}//App
class Utl {//c
static accessBy( object /*keys*/ ) {//m
for( let i = 1; i < arguments.length; i++ ) {
object = object[ arguments[ i ] ];
if( object == null ) return null;
if( typeof object === "undefined" ) return undefined;
}
return object;
}
static array2object( array, object, keyMaker ) {//m
for( let element of array ) {
object[ keyMaker( element ) ] = element;
}
}
static objectFrom( array, keyMaker ) {//m
let object = new Object();
for( let element of array ) {
object[ keyMaker( element ) ] = element;
}
return object;
}
static maxLength( strArray ) {//m
let v = 0;
for( let str of strArray ) v = Math.max( v, str.length );
return v;
}
}
class CmdElement {//c
constructor( col, row, cols, rows, fontSize, app ) {
this.col = col;
this.row = row;
this.cols = cols;
this.rows = rows;
this.fontSize = fontSize;
this.app = app;
this.x = this.col * this.fontSize;
this.y = this.row * this.fontSize;
this.width = this.cols * this.fontSize;
this.height = this.rows * this.fontSize;
this.elementType = "cmdelement";
}
typeKey( key ) {//m
}
draw( cc ) {//m
//ウィンドウ
let x = this.x - this.fontSize / 2;
let y = this.y - this.fontSize / 2;
let w = this.width + this.fontSize;
let h = this.height + this.fontSize;
//ウィンドウ周囲に影
if( 1 ) {
let mv = 2;
let mh = 3;
cc.globalAlpha = 0.2;
cc.fillStyle = "black";
cc.fillRect( x - mh, y - mv, w + mh * 2, h + mv * 2 );
cc.globalAlpha = 1;
}
cc.fillStyle = "white";
cc.strokeStyle = "black";
cc.fillRect( x, y, w, h );
cc.strokeRect( x, y, w, h );
}
}
class Menu extends CmdElement {//c
constructor( src, col, row, app ) {
//1文字を単位としたときの行と列
let itemNames = src.items.map( item => item.name );
let cols = Utl.maxLength( itemNames.concat( src.title ) ) + 1;
let rows = src.items.length;
//check.
if( src.title ) rows++;
super( col, row, cols, rows, 8, app );
this.elementType = "menu";
this.src = src;
this.itemCols = 1;
this.itemRows = this.src.items.length;
this.cursorX = 0;
this.cursorY = 0;
}//Menu
typeKey( key ) {//m
let addX = ( key == 39 ) - ( key == 37 );
let addY = ( key == 40 ) - ( key == 38 );
if( addX || addY ) {
this.cursorX += addX;
this.cursorY += addY;
//check.
if( this.cursorY < 0 ) this.cursorY = 0;
if( this.cursorY >= this.itemRows ) this.cursorY = this.itemRows - 1;
//カーソル 動いた時点で「点滅の点灯開始状態」にする
//こうしないとチラチラして見づらい
this.app.metronomez.forCursor.toggle = true;
this.app.metronomez.forCursor.time = 0;
} else {
this.selectedItem = this.src.items[ this.cursorY ];
let col, row;
switch( key ) {
case 32://SPACE
this.app.artz.soundz.pushA.rapidPlay();
let menusrc;
//選択した項目はメニューである
if( this.selectedItem.items ) {
menusrc = this.selectedItem;
} else if( this.src.nextMenu ) {
//選択した項目はメニューではないが、
//this.src内にnextMenu指定がある。
menusrc = {
title : this.src.nextMenu.title, //どれ?
items : this.selectedItem[ this.src.name ], //その人[道具],
col : this.src.nextMenu.col,
row : this.src.nextMenu.row,
}
} else if( this.selectedItem.nextMenu ) {
//選択した項目はメニューではないが、
//this.selectedItem内にnextMenu指定がある。
menusrc = this.selectedItem.nextMenu;
} else {
//メニューの終了
this.app.cmd.execMenu();
if( this.endmark ) this.endmark( this.selectedItem );
return;
}
col = typeof menusrc.col === "undefined"
? ( this.col + this.selectedItem.name.length + 2 )
: menusrc.col;
row = typeof menusrc.row === "undefined"
? ( this.row + this.cursorY + 2 )
: menusrc.row;
let menu = new Menu( menusrc, col, row, this.app );
this.app.cmd.addElement( menu, this );
break;
case 66://B
if( this.endmark ) this.endmark( null );//ショップ等 ストーリー時
else this.app.cmd.closeElement( this ); //キャンプ等 非ストーリー時
break;
default:
console.log( key );
}//switch
}//if else
}//typeKey
draw( cc ) {//m
super.draw( cc );
cc.save();
cc.translate( this.x + this.fontSize, this.y );
cc.fillStyle = "black";
cc.font = this.fontSize + "px ''";
//タイトル
if( this.src.title ) {
cc.fillText( this.src.title, -this.fontSize * 0.3, this.fontSize * 0.65 );
cc.translate( 0, this.fontSize );
}
//項目
let i = 0;
for( let item of this.src.items ) {
let gx = 0;
let gy = ( i + 1 ) * this.fontSize;
cc.fillText( item.name, gx, gy );
i++;
}
//カーソル
if( this.app.cmd.currentElement != this ||
this.app.metronomez.forCursor.toggle ) {
let w = this.fontSize * .7;
let h = this.fontSize * .8;
let gx = - this.fontSize + ( this.fontSize - w ) / 2;
let gy = this.cursorY * this.fontSize + ( this.fontSize - h ) / 2 + 1;
cc.beginPath();
cc.moveTo( gx, gy );
cc.lineTo( gx + w, gy + h / 2 );
cc.lineTo( gx, gy + h );
cc.closePath();
cc.fillStyle = "black";
cc.fill();
}
cc.restore();
}
}
class MessageWindow extends CmdElement {//c
constructor( app ) {
super( 1, 13, 19, 5, 12, app );
this.elementType = "messagewindow";
this.matrix = new Array();
for( let r = 0; r < this.rows; r++ ) {
this.matrix.push( new Array() );
}
this.matrixCursorCol = 0;
this.matrixCursorRow = 0;
}
write( message, flgs ) {//m
//check.
if( ! flgs ) {
flgs = { nokey : false, close : false }
}
this.message = message;
this.flgs = flgs;
return new Promise( function( endmark ) {
//1文字ずつ表示するアニメーション
this.endmark = endmark;
this.metronome = this.app.addMetronome( "test1", 50 );
this.seek = 0;
this.app.anms.push( function() {
//check. メトロノームに合わせる
if( ! this.metronome.changed ) return;
let ch = this.message.substr( this.seek, 1 );
let kaigyo = false;
switch( ch ) {
case "\n": kaigyo = true; break;
default:
this.matrix[ this.matrixCursorRow ][ this.matrixCursorCol ] = ch;
this.matrixCursorCol ++;
//check. 右端
if( this.matrixCursorCol == this.cols ) kaigyo = true;
}//switch
//check. 改行
if( kaigyo ) {
this.matrixCursorCol = 0;
//check. 下端
if( this.matrixCursorRow < this.rows - 1 ) {
this.matrixCursorRow ++;
} else {
//1行スクロール
this.matrix.shift();
this.matrix.push( new Array() );
}
}
this.seek ++;
//check. メッセージ出力終了
if( this.seek == this.message.length ) {
if( this.flgs.nokey ) this.endmark();
return true;
}
}.bind( this ) );//anm
}.bind( this ) ).then( function() {
//endmark()されたらここに来る
if( this.flgs.close ) {
this.app.cmd.closeElement( this.app.messageWindow );
}
}.bind( this ) );//new Promise
}//write
typeKey( key ) {
if( this.seek < this.message.length ) {
//文字出力中のときは文字出力を一気に行う
this.seek = this.message.length - 1;
} else if( ! this.flgs.nokey ) {
//文字出力終了のときは、呼び出し元に処理終了を知らせる。
this.endmark();
}
}
draw( cc ) {//m
super.draw( cc );
cc.save();
cc.translate( this.x, this.y );
cc.font = this.fontSize + "px ''";
cc.fillStyle = "black";
for( let r = 0; r < this.rows; r++ ) {
let gy = this.fontSize * r;
for( let c = 0; c < this.cols; c++ ) {
let gx = this.fontSize * c;
let ch = this.matrix[ r ][ c ];
//check.
if( ! ch ) continue;
cc.fillText( ch, gx, gy + this.fontSize );
}
}
cc.restore();
}
}
class Screen {//c
constructor( app ) {//m
this.app = app;
this.actions = new Array();
this.style = {
visibility : false,
}
}
start() {
}
actionsPush() {//m
this.actions.push( Array.from( arguments ).join( "," ) );
}
searchStory() {//m
}
senseKey( key ) {
}
typeKey( key ) {
}
draw( cc ) {
}
}
class CmdScreen extends Screen {//c
constructor( app ) {//m
super( app );
this.style.visibility = false;
this.elements = new Array();
this.menus = new Array();
}
start( menusrc ) {//m
let menu = new Menu( menusrc, 1, 1, this.app );
this.app.cmd.addElement( menu );
}
addElement( element, parent ) {//m
this.elements.push( element );
this.currentElement = element;
//check. Menuならmenusにも登録
if( element instanceof Menu ) {
this.menus.push( element );
} else if( element instanceof MessageWindow ) {
this.app.messageWindow = element;
}
element.parent = parent;
}
closeElement( element ) {//m
this.elements.splice( this.elements.indexOf( element ), 1 );
//check. 物によっては他の場所でも削除
if( element instanceof Menu ) {
this.menus.splice( this.menus.indexOf( element ), 1 );
} else if( element instanceof MessageWindow ) {
delete this.app.messageWindow;
}
//削除後
if( element.parent ) {
//親の要素へ戻る
this.currentElement = element.parent;
} else if( this.elements.length ) {
//ひとつ前の要素へ戻る
this.currentElement = this.elements[ this.elements.length - 1 ];
} else {
//画面に何もなくなったら、マップ画面へ戻る
this.app.currentScreen = this.app.map;
}
}
async execMenu() {//m
let script;
//メニュー順に引数を配置する方式
let args = new Array();
for( let menu of this.menus ) {
if( menu.selectedItem.script ) script = menu.selectedItem.script;
args.push( menu.selectedItem );
}
if( script ) {
await script.call( this.app, ...args );
//check scriptによってサブメニューの元となったモノがなくなった。
for( let i = this.menus.length - 1; i > 0; i -- ) {
let menu = this.menus[ i ];
let selfItem = menu.parent.selectedItem;
//親メニューに自身の選択項目がない。(薬草を使ったから薬草が消えた)
if( menu.parent.src.items.indexOf( selfItem ) == -1 ) {
//自身にまつわるサブメニューを消去。(薬草をどうする等サブメニューを消去)
for( let j = this.menus.length - 1; j >= i; j-- ) {
this.closeElement( this.menus[ j ] );
}
}
}
}
}
typeKey( key ) {//m
this.currentElement.typeKey( key );
}
draw( cc ) {//m
for( let element of this.elements ) {
element.draw( cc );
}
cc.font = "10px ''";
cc.fillStyle = "red";
cc.fillText( this.currentElement.elementType, 0, 10 );
}
}//CmdScreen
class MapScreen extends Screen {//c
/*
マップの表示という意味ではなく、
フィールド画面の総合的処理という意味。
*/
constructor( app ) {//m
super( app );
this.cellSize = 16;
this.cols = 17;
this.rows = 15;
this.colsHalf = Math.floor( this.cols / 2 );
this.rowsHalf = Math.floor( this.rows / 2 );
this.scroll = {
count : 0,
isAnimating : false,
tweakX : 0,
tweakY : 0,
}
this.keyLock = false;
}//constructor
treatX( x ) {
//tweak. ループ時は範囲外の値を範囲内に調整する
if( this.cfg.maploop ) {
if( x < 0 )
x += this.mapWidth;
else if( x >= this.mapWidth )
x -= this.mapWidth;
}
return x;
}
treatY( y ) {
//tweak. ループ時は範囲外の値を範囲内に調整する
if( this.cfg.maploop ) {
if( y < 0 )
y += this.mapHeight;
else if( y >= this.mapHeight )
y -= this.mapHeight;
}
return y;
}
typeKey( key ) {//m
if( key == 32 ) {
//向いている方向に何かがある
let pos = this.getPositionBy( 1 );
let bit;
if( bit = Utl.accessBy( this.mapBits, pos.y, pos.x ) ) {
if( bit == "□" ) {
pos = this.getPositionBy( 2 );
this.actions.push( { x : pos.x, y : pos.y, type : "contact" } );
return;
} else {
}
}
this.app.artz.soundz.pushA.rapidPlay();
this.app.cmd.style.visibility = true;
this.app.currentScreen = this.app.cmd;
let menu = new Menu( this.app.menusrcz.camp, 1, 1, this.app );
this.app.cmd.addElement( menu );
}
}
searchStory() {//m
let story = null;
for( let action of this.actions ) {
//ストーリー マップの外に出た
if( action.type == "got_on" && action.x == -1 && action.y == -1 ) {
story = this.stories[ "outer,got_on" ];
break;
}
//ストーリー その他
if( story = Utl.accessBy( this.storyBits, action.y, action.x, "story", action.type ) ) break;
}
this.actions.length = 0;
return story;
}
getPositionBy( step ) {//m
let dir = this.app.status.player.direction;
let x = this.sx + ( ( dir == 2 ) - ( dir == 0 ) ) * step;
let y = this.sy + ( ( dir == 3 ) - ( dir == 1 ) ) * step;
return { x : x, y : y }
}
senseKey( key ) {//m
//check.
if( this.scroll.isAnimating || this.keyLock ) return;
let addX, addY;
switch( key ) {
case 37: this.app.status.player.direction = 0; addX = -1; addY = 0; break;
case 38: this.app.status.player.direction = 1; addX = 0; addY = -1; break;
case 39: this.app.status.player.direction = 2; addX = 1; addY = 0; break;
case 40: this.app.status.player.direction = 3; addX = 0; addY = 1; break;
default: return;
}
//check. 移動先は壁
let sx = this.treatX( this.sx + addX );
let sy = this.treatY( this.sy + addY );
let bit;
if( bit = Utl.accessBy( this.mapBits, sy, sx ) )
if( this.cfg.walls.indexOf( bit ) > -1 ) {
this.app.artz.soundz.pushWall.play();
return;
}
//1マススクロールの起動
this.scroll.addX = addX;
this.scroll.addY = addY;
this.scroll.count = 0;
this.scroll.isAnimating = true;
}//senseKey
frameForScroll() {//m
//1マススクロールにおける、1ドット分のスクロール処理
this.scroll.tweakX -= this.scroll.addX;
this.scroll.tweakY -= this.scroll.addY;
//tweakX,Yが1ドットスクロールの主要のしくみ
//それについてはthis.draw()を参照してください。
this.scroll.count ++;
//check. スクロールの終了
if( this.scroll.count == this.cellSize ) {
this.scroll.isAnimating = false;
this.scroll.tweakX = 0;
this.scroll.tweakY = 0;
//draw()はスクロール終了後も参照するからここで0にする。
this.sx = this.treatX( this.sx + this.scroll.addX );
this.sy = this.treatY( this.sy + this.scroll.addY );
//マップ外に乗ったストーリー発生
if( ! this.cfg.maploop
&& ( this.sx < 0 || this.sx >= this.mapWidth
|| this.sy < 0 || this.sy >= this.mapHeight )
) {
this.actions.push( { x : -1, y : -1, type : "got_on" } );
} else {
//座標に乗ったストーリー発生
this.actions.push( { x : this.sx, y : this.sy, type : "got_on" } );
}
}//if スクロール終了
}
playerMapMoveTo_fx( mapdatafunc /*other args*/ ) {//m
//視覚効果付きのマップ間移動
let argArray = Array.from( arguments );
//check. 歩く音
let donotFootsteps = false;
if( ( typeof argArray[ argArray.length - 1 ] ) == "boolean" ) {
donotFootsteps = argArray.pop();
}
if( ! donotFootsteps ) this.app.artz.soundz.walk.play();
this.keyLock = true;
//クロージャ(関数が終わっても、この関数内で定義した関数内では生き続ける変数)
let countMax = 3;
let count = countMax;
let flg = true;
//マップ間移動時の画面フェードアウト、イン
let metronome = this.app.addMetronome( "forFade", 125 );
this.app.anms.push( function() {
//check. メトロノームに合わせて
if( ! metronome.changed ) return;
if( flg ) {
//フェードアウト
count --;
let value = 100 * count / countMax;
this.app.cc.canvas.style.filter = "brightness( " + value + "% ) ";
//check. フェードアウトの終了
if( ! count ) {
//マップ間移動する
this.playerMapMoveTo( ...argArray );
flg = false;
count = countMax;
}
} else {
//フェードイン
count --;
let value = 100 * ( countMax - count ) / countMax;
this.app.cc.canvas.style.filter = "brightness( " + value + "% ) ";
//check. フェードインの終了(全終了)
if( ! count ) {
this.keyLock = false;
delete this.app.metronomez.forFade;
return true; //trueはアニメの終了の意
}
}
}.bind( this ) );
}
playerMapMoveTo( mapdatafunc /*other args*/ ) {//m
//マップ間移動
//check.
if( this.app.bgm ) {
this.app.bgm.pause();
this.app.bgm.currentTime = 0;
}
//データを記述した関数からデータを取り出す1
let commentData = mapdatafunc.toString().match( /\/\*\r\n([\s\S]+)\r\n\*\// )[ 1 ];
//データを記述した関数からデータを取り出す2
this.cfg = mapdatafunc();
//取り出したデータを使えるように加工
this.mapBits = new Array();
this.storyBits = new Array();
this.stories = new Object();
//マップデータを読み取る ここから
let storyTmp = new Array();
let lines = commentData.split( /\r\n/ );
for( let y = 0; y < lines.length; y++ ) {
let line = lines[ y ];
this.mapBits[ y ] = new Array();
for( let x = 0; x < line.length; x++ ) {
let bgBit = line.substr( x, 1 );
//check. 半角文字のときはストーリーへのショートカット
if( bgBit.match( /[0-9a-z ]/i ) ) {
let shortcutId = bgBit + line.substr( x + 1, 1 );
shortcutId = shortcutId.replace( / /, "" );
//check. ショートカットがリンク切れ
if( ! this.cfg.shortcuts[ shortcutId ] ) {
alert( "マップ上に配置したショートカットがリンク切れ id:" + shortcutId );
continue;
}
//マップ上のショートカットについては後で処理
storyTmp.push( {
shortcutId : shortcutId,
x : x,
y : y,
} );
//ショートカットをマップデータに置き換え
bgBit = this.cfg.shortcuts[ shortcutId ].bit;
x++; //1文字多く読んだから
}
this.mapBits[ y ].push( bgBit );
}
}//for
this.mapWidth = this.mapBits[ 0 ].length;
this.mapHeight = this.mapBits.length;
//storyBits初期化
for( let y = 0; y < this.mapHeight; y++ ) {
this.storyBits[ y ] = new Array();
for( let x = 0; x < this.mapWidth; x++ ) {
this.storyBits[ y ][ x ] = null;
}
}
//storyBitsにstoryを配置
for( let tmp of storyTmp ) {
let shortcut = this.cfg.shortcuts[ tmp.shortcutId ];
this.storyBits[ tmp.y ][ tmp.x ] = shortcut;
//tweak. 座標情報をセット
shortcut.x = tmp.x;
shortcut.y = tmp.y;
}
//マップデータを読み取る ここまで
//BGM
if( this.cfg.bgmTitle ) {
this.app.bgm = this.app.artz.bgms[ this.cfg.bgmTitle ];
this.app.bgm.loop = true;
this.app.bgm.play();
}
//マップの外に出たときのストーリー
if( ! this.cfg.maploop ) {
let story = this.cfg.shortcuts.outer.story.got_on;
this.stories[ "outer,got_on" ] = story;
}
//メソッドの引数によって処理を分ける(オーバーロードみたいに)
let argtypes;
let argArray = Array.from( arguments );
argtypes = argArray.map( arg => ( typeof arg ).substr( 0, 1 ) ).join( "" );
//ショートカットを指定して移動
if( argtypes.indexOf( "fs" ) == 0 ) {
//playerMapMoveTo( function, string [,number] )
let shortcut = arguments[ 1 ];
this.sx = this.cfg.shortcuts[ shortcut ].x;
this.sy = this.cfg.shortcuts[ shortcut ].y;
//check.
if( typeof arguments[ 2 ] !== "undefined" )
this.app.status.player.direction = arguments[ 2 ];
} else if( argtypes.indexOf( "fnn" ) == 0 ) {
//移動先位置の座標指定して移動
//playerMapMoveTo( function, number, number [,number] )
this.sx = arguments[ 1 ];
this.sy = arguments[ 2 ];
//check.
if( typeof arguments[ 3 ] !== "undefined" )
this.app.status.player.direction = arguments[ 3 ];
} else {
alert( "playerMapMoveTo()の引数リストが想定外 : " + argtypes );
}
}//playerMapMoveTo
draw( cc ) {//m
cc.clearRect( 0, 0, 256, 224 );
//this.sx,this.syが画面中央に来るように描画開始座標を調整
let sx = this.sx - this.colsHalf + this.mapWidth;
let sy = this.sy - this.rowsHalf + this.mapHeight;
//+ this.mapWidth,Heightは、下の計算で % this.mapWidth,Height
//としたときに負値になるのを避けるため。
cc.save();
cc.translate( -this.cellSize / 2 + this.scroll.tweakX, -this.cellSize / 2 + this.scroll.tweakY );
//tweakX,Yが描画位置を1ドットずつずらすので
//きれいに1ドットスクロールしてるようにみえる
cc.font = this.cellSize + "px ''";
cc.fillStyle = "rgb(0,255,0)";
for( let row = 0; row < this.rows; row++ ) {
let y = ( sy + row ) % this.mapHeight; //端を超えると反対側に
let gy = row * this.cellSize;
for( let col = 0; col < this.cols; col++ ) {
let x = ( sx + col ) % this.mapWidth; //端を超えると反対側に
let bit = this.mapBits[ y ][ x ];
let gx = col * this.cellSize;
let image = this.cfg.images[ bit ] ? this.cfg.images[ bit ] : bit;
cc.fillText( image, gx, gy + this.cellSize );
}
}
//スプライト描画
for( let row = 0; row < this.rows; row++ ) {
let y = ( sy + row ) % this.mapHeight; //端を超えると反対側に
let gy = row * this.cellSize;
for( let col = 0; col < this.cols; col++ ) {
let x = ( sx + col ) % this.mapWidth; //端を超えると反対側に
let story = this.storyBits[ y ][ x ];
//check.
if( ! story ) continue;
if( ! story.sprite ) continue;
let gx = col * this.cellSize;
let image = story.sprite;
cc.fillText( image, gx, gy + this.cellSize );
}
}
cc.restore();
//プレイヤーキャラ描画
cc.fillStyle = "cyan";
cc.font = this.cellSize + "px ''";
let gx = ( cc.canvas.width - this.cellSize ) / 2;
let gy = ( cc.canvas.height - this.cellSize ) / 2 + this.cellSize;
let sprite = this.app.artz.spritez.player;
let idx = this.app.status.player.direction * 2 + sprite.metronome.index;
let image = sprite.images[ idx ];
cc.fillText( image, gx, gy );
}//draw
}//MapScreen
class TitleScreen extends Screen {//c
constructor( app ) {//m
super( app );
this.name = "title";
}
async typeKey( key ) {//m
this.app.artz.soundz.pushA.play();
//マップ画面を表示する。
let map = this.app.status.player.position.map;
let x = this.app.status.player.position.x;
let y = this.app.status.player.position.y;
// this.app.screenz.map.playerMapMoveTo( map, x, y );
this.app.screenz.map.playerMapMoveTo( App.townmap, 3, 6 );
this.app.currentScreen = this.app.screenz.map;
this.app.screenz.map.style.visibility = true;
this.style.visibility = false;
await this.app.tell( "スタート", { close:true } );
}
draw( cc ) {//m
cc.clearRect( 0, 0, cc.canvas.width, cc.canvas.height );
cc.fillStyle = "white";
cc.fillText( "hit any key", 100, 100 );
}
}
App.worldmap = function() {/*
●〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃¥¥¥▲〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
¥山山山山山〃~~~~~~¥¥〃〃〃〃〃〃〃〃〃¥¥山山山¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
¥山山山〃〃〃〃~~~〃〃〃〃〃〃〃〃〃〃〃〃¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
¥山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
¥¥〃〃1 〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃〃〃山山〃〃〃〃〃〃〃¥¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃山山山〃〃〃〃〃〃〃¥¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃山山山山山〃〃〃〃〃〃〃〃¥¥山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃山山山〃〃〃〃〃〃〃〃¥〃〃¥山山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥¥〃〃〃〃山山山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥〃〃〃〃〃山山山山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
山山山〃〃〃~~~〃〃〃〃〃〃〃〃〃〃山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
山山〃〃~~~~¥¥〃〃〃〃〃〃〃〃山山山山〃〃〃〃〃〃〃山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
山〃〃~~~¥¥¥〃〃〃〃〃〃〃〃山山山山山山山〃〃〃〃〃山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃¥¥¥¥〃〃〃〃〃〃〃〃〃山山山山山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃¥¥¥〃〃〃〃〃〃〃山山山山山山山山山山~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃¥¥¥〃〃〃〃〃〃山山山山山~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃¥¥¥〃〃〃〃〃〃〃〃〃〃~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃¥¥¥¥¥¥¥〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃¥¥¥¥¥¥¥¥〃〃〃〃〃〃〃〃~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃¥¥¥¥¥¥〃〃〃〃〃〃〃〃〃~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃¥¥¥¥¥〃〃〃〃〃〃〃〃〃〃~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃¥¥〃〃〃〃〃〃〃〃〃〃~~~~~〃〃〃〃〃〃〃¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
*/
return {
maploop : true,
bgmTitle : "field",
walls : "~",
shortcuts : {
"1" : {
bit : "凸",
story : {
"got_on" : {
begin : function( e ) {
this.screenz.map.playerMapMoveTo_fx( App.townmap, "s", 2 );
},
},
},
},
},
images : {
"¥" : "🌲",
"凸" : "🏰",
"山" : "⛰",
"~" : "🌊",
},
}//return
}//worldmap
App.townmap = function() {/*
木木木木木木木〃・・〃木木木木木木木木木〃・・〃〃〃〃〃〃〃
木■■■■■木〃・・〃木■■■■■■■木〃・・〃〃〃〃〃〃〃
木■刀刀刀■木〃・・〃木■■■■■■■木〃・・〃〃〃〃〃〃〃
木■・ws・■木〃・・〃木■■■■■■■木〃・・〃〃〃〃〃〃〃
木■■□■■木〃・・〃木■■■扉■■■木〃・・〃〃〃〃〃〃〃
木木〃・剣木木〃・・〃〃花花花・花花花〃〃・・〃〃〃〃〃〃〃
s ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・
・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・
木木盾・〃木木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
木■■□■■木〃・・〃a1〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
木■・ss・■木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
木■盾盾盾■木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
木■■■■■木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
木木木木木木木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
*/
return {
maploop : false,
bgmTitle : "town",
walls : "■□扉",
shortcuts : {
outer : {
bit : "〃",
story : {
"got_on" : {
begin : function( e ) {
this.screenz.map.playerMapMoveTo_fx( App.worldmap, "1", 3 );
},
},
},
},
"s" : {
bit : "・",
},
"ws" : {
bit : "👨🔧",
story : {
"contact" : {
begin : async function( e ) {
await this.tell( "ここは刀剣屋だ。\n" );
await this.shop();
}
},
},
},
"a1" : {
bit : "〃",
sprite : "🕵️♂️",
story : {
"contact" : {
begin : function( e ) {
this.tell( "ドラゴンなら北のほうへ飛んで行ったぜ", true );
}
},
},
},
"ss" : {
bit : "👨🔧",
},
},
images : {
"木" : "🌳",
"花" : "🌷",
"扉" : "🚪",
"箱" : "📦",
"座" : "🪑",
"寝" : "🛏",
"∪" : "🏺",
"剣" : "⚔",
"刀" : "🗡",
"盾" : "🛡",
},
}//return
}//townmap
※プログラムリスト中の小さな画像は「絵文字」です。絵文字がRPGのキャラの即席の代用に使えると思って利用しています。
(訪問者のどんなニーズと この記事がつながるか)
最近私は、「全体的・基本・簡潔のRPG」という名目で 自作のロールプレイングゲームを作っています。
昔からの趣味で、RPG (ロールプレイングゲーム)について その部品みたいなプログラム(マップを表示するだけ、キャラが上下左右に動くだけ、戦闘画面だけ等)をいろいろ作ってきましたが、30年目にしてやっと、部品ではなくて全体的なものを作り始めた感じです。
RPG開発の「原点」または「ひな形」、「テンプレート」となるように、ごく基本的で簡潔なプログラムで作ろうとしています。
現在は、「コマンドメニュー作成85%」の状態です。プログラム言語は JavaScript。
このスクリーンショットではコマンドメニューを展開しているところが写されています。
コマンドメニューについては以前 研究していたので、わりと つまづかずにプログラムできています。
RPGってプログラムとしては だいぶ複雑だなぁ、と感じています。
高校生のときの自分には無理だったんじゃないか、と思います。
その高校生の自分には無理だった、というプログラムリストがこれ↓
この青い部分をダブルクリックするたびに、リストの全体を表示/非表示します。
タッチデバイスの場合はタッチします。
class App {//c
constructor( canvas ) {
this.name = "app";
this.cc = canvas.getContext( "2d", { alpha : false } );
this.cc.canvas.width = 256;
this.cc.canvas.height = 224;
this.cellSize = 16;
//---メトロノーム
this.metronomes = new Object();
this.addMetronome( "t1000_i2" );
this.addMetronome( "t500_i2" );
this.addMetronome( "t250_i2" );
//カーソル専用(手動追加)
this.metronomes.forCursor = {
maxTime : 500,
maxIndex : 0,
time : 0,
index : 0,
toggle : true,
changed : false,
}
/*
メトロノームの各フラグの変化タイミング図解(タイミングチャート)
maxTime : 250,
maxIndex : 3, の場合の各フラグの動き↓
(経過時間) 0 ... 250 ... 500 ... 750 ... 1000 ...(ms)
time : 0, 0 ..249 0 ..249 0 ..249 0 ..249 0 .. ...(ms)
index : 0, 000000001111111122222222000000001111 ...
toggle : true, ~~~~~~~~________~~~~~~~~________~~~~ ...
changed : false, ________~_______~_______~_______~___ ...
~ : true
_ : false
... : 略
※このグラフ中の1文字単位の縦1列はrequestAnimationFrameに相当する
つまり、requestAnimationFrameごとに、
timeは経過時間を増やしていきmaxTimeになると0に戻る
indexはtimeがmaxTimeになるごとに1増え、maxIndexになると0に戻る
toggleはtimeがmaxTimeになるごとに真偽を反転する
changedはtimeがmaxTimeになったときだけ真になる
maxTimeでアニメのパラパラのスピード決め
toggleやchangedでアニメのパラパラの実行を判断
indexでアニメのパラパラの画の切り替え
*/
this.sprites = {
player : {
images : [ "🦸♂️", "🦸♂️" ],
// images : [ "🙍", "🙎" ],
metronome : this.metronomes.t500_i2,
},
}
this.anms = new Array();
this.artz = { //ex. resources
sprites : this.sprites,
bgms : new Object(),
soundz : new Object(),
}
this.screenz = {
title : new TitleScreen( this ),
map : new MapScreen( this ),
cmd : new CmdScreen( this ),
}
//各画面について
for( let name in this.screenz ) {
let screen = this.screenz[ name ];
//メソッドのthisをappにする
// this.screenz[ name ] = this.changeForApp( screen );
//ショートカット
this[ name ] = screen;
}
//---キャラクター
this.characters = new Array();
this.characters.push( {
name : "キャラ1",
"どうぐ" : [
{
name : "なんこう1",
nextMenu : {
title : "どうする?",
items : [
{
name : "つかう",
title : "だれに?",
items : this.characters,
script : async function( cmd, char1, dougu, cmd2, char2 ) {
await this.typeMessage( char1.name + "は" + char2.name + "に" + dougu.name + "を使った!" );
},
},
{
name : "わたす",
title : "だれに?",
items : this.characters,
script : function( cmd, char1, dougu, char2 ) {
console.log( char1.name + "は" + char2.name + "に" + dougu.name + "をわたした" );
},
},
{
name : "すてる",
script : function( cmd, char1, dougu, char2 ) {
console.log( char1.name + "は" + dougu.name + "を捨てた!" );
},
},
],
},
},
{ name : "つるぎ" },
],
} );
this.characters.push( {
name : "キャラ2",
"どうぐ" : [
{ name : "なんこう2" },
{ name : "つるぎ" },
],
} );
this.characters.push( {
name : "キャラ3",
"どうぐ" : [
{ name : "なんこう3" },
{ name : "つるぎ" },
],
} );
this.characterz = Utl.objectFrom( this.characters, element => element.name );
//---メニュー
this.menusrcs = [
{
name : "camp",
title : "コマンド?",
items : [
{
name : "どうぐ",
title : "だれの?",
items : this.characters,
nextMenu : {
title : "どれ?",
row : 3,
},
},
{
name : "しらべる",
},
{
name : "まほう",
title : "だれの?",
items : this.characters,
},
],
},
]
this.menusrcz = Utl.objectFrom( this.menusrcs, element => element.name );
//---ストーリー
//(プログラムのイベントとの競合を避けるため、ゲームのシナリオイベントのことをストーリーと呼んでいる)
this.screenz.title.sysStories = {
"selected_start" : { //タイトル画面でスタートが選ばれたら
begin : function() {
//マップ画面を表示する。
let map = this.status.position.map;
let x = this.status.player.position.x;
let y = this.status.player.position.y;
this.screenz.map.playerMapMoveTo( map, x, y );
this.currentScreen = this.screenz.map;
this.screenz.map.style.visibility = true;
this.screenz.title.style.visibility = false;
},
},
}
this.screenz.map.sysStories = {
"pushA" : {
begin : function() {
//コマンド画面を表示する。
this.currentScreen = this.screenz.cmd;
this.screenz.cmd.style.visibility = true;
},
},
}
this.screenz.cmd.sysStories = {
"pushB" : {
begin : function() {
//マップ画面を表示する。
this.currentScreen = this.screenz.map;
this.screenz.cmd.style.visibility = false;
},
},
}
//素材のプリロード
return Promise.all( [
this.load( "bgm", "field", "_bgm/field.wav" ),
this.load( "bgm", "town", "_bgm/town.wav" ),
this.load( "bgm", "battle", "_bgm/battle.wav" ),
this.load( "sound", "pushA", "_sound/pushA.mp3" ),
this.load( "sound", "pushWall", "_sound/pushWall.mp3" ),
this.load( "sound", "walk", "_sound/walk.wav" ),
] ).then( function() {
return this; //ここはコンストラクタなのでAppのインスタンスを返すこと
}.bind( this ) );
}//constructor()
addMetronome( name /*other args*/ ) {//m
//オーバーロード(メソッド名が同じで引数のパターンが異なる)の模造
//”関数の先頭で引数の型を判定する条件分岐で対応”
let argsId = "";
for( let arg of Array.from( arguments ) ) {
argsId += "," + ( typeof arg ).substr( 0, 1 );
}
let metronome;
switch( argsId ) {
case ",s" : //フォーマット名称 t時間_iインデックス上限
let tokens = name.split( /_/ );
metronome = {
maxTime : Number( tokens[ 0 ].substr( 1 ) ),
maxIndex : Number( tokens[ 1 ].substr( 1 ) ),
time : 0,
index : 0,
toggle : false,
changed : false,
}
this.metronomes[ name ] = metronome;
break;
case ",s,n" : //自由な名称、時間
let tm = arguments[ 1 ];
metronome = {
maxTime : tm,
maxIndex : 0,
time : 0,
index : 0,
toggle : false,
changed : false,
}
this.metronomes[ name ] = metronome;
break;
default:
alert( "addMetronome()にて、未定義の引数リスト:[" + args + "]" );
}
return metronome;
}//addMetronome()
typeMessage( message ) {//m
//check. メッセージウィンドウがないときは作成する
if( ! this.cmd.messageWindow ) {
this.cmd.messageWindow = new MessageWindow( this );
this.cmd.addElement( this.cmd.messageWindow );
}
//check. コマンド画面を有効にする
if( this.currentScreen != this.cmd ) {
this.currentScreen = this.cmd;
this.cmd.style.visibility = true;
}
let mw = this.cmd.messageWindow;
return new Promise( function( aizu ) {
let metronome = this.addMetronome( "test1", 50 );
mw.write( message, metronome, aizu );
}.bind( this ) ).then( function() {
this.cmd.closeElement( mw );
delete this.cmd.messageWindow;
}.bind( this ) );
}
//プリロード
load( type, name, src ) {//m
return new Promise( function( promiseOK ) {
let object;
console.log( type, name, "loading.." );
switch( type ) {
case "bgm":
object = new Audio();
object.oncanplaythrough = function() {
this.artz.bgms[ name ] = object;
object.oncanplaythrough = null;
console.log( "\t", type, name, "loaded." );
promiseOK();
}.bind( this );
object.src = src;
object.load();
break;
case "sound":
object = new Audio();
object.oncanplaythrough = function() {
this.artz.soundz[ name ] = object;
object.oncanplaythrough = null;
console.log( "\t", type, name, "loaded." );
object.rapidPlay = function() {
object.pause();
object.currentTime = 0;
object.play();
/*
同じ音楽ファイルの連続演奏時は
最初にリセットをかける必要がある。
rapid=急速
*/
}
promiseOK();
}.bind( this );
object.src = src;
object.load();
break;
}
}.bind( this ) );
}//load()
async start() {//m
this.keys = new Array();
onkeydown = function( e ) {
if( this.keys.indexOf( e.which ) == -1 ) {
this.keys.push( e.which ); //キーセンス オン
this.currentScreen.typeKey( e.which, this );
//キータイプ 実行
}
}.bind( this );
onkeyup = function( e ) {
let idx = this.keys.indexOf( e.which );
if( idx > -1 ) this.keys[ idx ] *= -1;
//キーセンス オフ予定にする(同じ数の負値)
}.bind( this );
this.status = {
player : {
position : {
map : App.worldmap,
x : 0,
y : 0,
},
},
}
//タイトル画面から始める
this.screenz.title.style.visibility = true;
this.currentScreen = this.screenz.title;
this.beforeTime = 0;
this.frame( 0 );
}//start()
frame( tm ) {//m
//経過時間
let diff = tm - this.beforeTime;
this.beforeTime = tm;
//メトロノーム書き換え
for( let key in this.metronomes ) {
let metronome = this.metronomes[ key ];
metronome.time += diff
//check. そのメトロノームは時を刻んだ
if( metronome.time >= metronome.maxTime ) {
metronome.toggle = ! metronome.toggle;
metronome.changed = true;
metronome.time = 0;
//check. インデックスを進める
metronome.index++;
if( metronome.index == metronome.maxIndex )
metronome.index = 0;
} else {
metronome.changed = false;
}
}
//キーセンス 処理
for( let i = this.keys.length - 1; i >= 0; i-- ) {
let key = this.keys[ i ];
this.currentScreen.senseKey( Math.abs( key ), this );
//キーセンス 実行
//check. キーセンスオフ予定ならキーセンス オフ
if( key < 0 ) this.keys.splice( i, 1 );
}
//1ドットスクロール実行
if( this.screenz.map.scroll.isAnimating ) {
this.screenz.map.frameForScroll();
}
//ストーリー検索&実行
let story;
if( story = this.currentScreen.searchStory() ) {
story.begin.call( this );
}
//アニメ処理
for( let i = this.anms.length - 1; i >= 0; i-- ) {
if( this.anms[ i ]() ) this.anms.splice( i, 1 );
}
this.draw( this.cc );
this.timerId = requestAnimationFrame( this.frame.bind( this ) );
}//frame()
draw( cc ) {//m
cc.clearRect( 0,0,cc.canvas.width, cc.canvas.height );
for( let screen of Object.values( this.screenz ) ) {
if( screen.style.visibility ) screen.draw( cc );
}
}
}//App
class Utl {//c
static objectFrom( array, keyMaker ) {//m
let object = new Object();
for( let element of array ) {
object[ keyMaker( element ) ] = element;
}
return object;
}
static maxLength( strArray ) {//m
let v = 0;
for( let str of strArray ) v = Math.max( v, str.length );
return v;
}
}
class CmdElement {
constructor( col, row, cols, rows, fontSize, app ) {
this.col = col;
this.row = row;
this.cols = cols;
this.rows = rows;
this.fontSize = fontSize;
this.app = app;
this.x = this.col * this.fontSize;
this.y = this.row * this.fontSize;
this.width = this.cols * this.fontSize;
this.height = this.rows * this.fontSize;
}
typeKey( key ) {
}
draw( cc ) {
//ウィンドウ
cc.fillStyle = "white";
cc.strokeStyle = "black";
let x = this.x - this.fontSize / 2;
let y = this.y - this.fontSize / 2;
let w = this.width + this.fontSize;
let h = this.height + this.fontSize;
cc.fillRect( x, y, w, h );
cc.strokeRect( x, y, w, h );
}
}
class Menu extends CmdElement {//c
constructor( src, col, row, app ) {
//1文字を単位としたときの行と列
let itemNames = src.items.map( item => item.name );
let cols = Utl.maxLength( itemNames.concat( src.title ) ) + 1;
let rows = src.items.length + 1;
super( col, row, cols, rows, 8, app );
this.src = src;
this.itemCols = 1;
this.itemRows = this.src.items.length;
this.cursorX = 0;
this.cursorY = 0;
}//Menu
typeKey( key ) {//m
let addX = ( key == 39 ) - ( key == 37 );
let addY = ( key == 40 ) - ( key == 38 );
if( addX || addY ) {
this.cursorX += addX;
this.cursorY += addY;
//check.
if( this.cursorY < 0 ) this.cursorY = 0;
if( this.cursorY >= this.itemRows ) this.cursorY = this.itemRows - 1;
//カーソル 動いた時点で「点滅の点灯開始状態」にする
//こうしないとチラチラして見づらい
this.app.metronomes.forCursor.toggle = true;
this.app.metronomes.forCursor.time = 0;
} else {
this.selectedItem = this.src.items[ this.cursorY ];
let col, row;
switch( key ) {
case 32://SPACE
this.app.artz.soundz.pushA.rapidPlay();
let menusrc;
//選択した項目はメニューである
if( this.selectedItem.items ) {
menusrc = this.selectedItem;
} else {
//選択した項目はメニューではない
//しかし、nextMenu指定がある(2か所のいずれか)
if( this.src.nextMenu ) {
//this.srcにnextMenu
menusrc = {
title : this.src.nextMenu.title, //どれ?
items : this.selectedItem[ this.src.name ], //その人[道具],
col : this.src.nextMenu.col,
row : this.src.nextMenu.row,
}
} else if( this.selectedItem.nextMenu ) {
//this.selectedItemにnextMenu
menusrc = this.selectedItem.nextMenu;
}
}
//次のメニューが決定された
if( menusrc ) {
col = typeof menusrc.col === "undefined"
? ( this.col + this.selectedItem.name.length + 2 )
: menusrc.col;
row = typeof menusrc.row === "undefined"
? ( this.row + this.cursorY + 2 )
: menusrc.row;
let menu = new Menu( menusrc, col, row, this.app );
this.app.cmd.addElement( menu );
} else {
//メニューの終了
this.app.cmd.execMenu();
}
break;
case 27://ESC
this.app.cmd.closeElement( this );
break;
default:
console.log( key );
}//switch
}//if else
}//typeKey
draw( cc ) {//m
super.draw( cc );
cc.save();
cc.translate( this.x + this.fontSize, this.y );
//タイトル
cc.font = this.fontSize + "px ''";
cc.fillStyle = "black";
cc.fillText( this.src.title, -this.fontSize * 0.3, this.fontSize * 0.65 );
//項目
let i = 0;
for( let item of this.src.items ) {
let gx = 0;
let gy = ( i + 2 ) * this.fontSize;
cc.fillText( item.name, gx, gy );
i++;
}
//カーソル
if( this.app.cmd.currentElement != this ||
this.app.metronomes.forCursor.toggle ) {
let w = this.fontSize * .7;
let h = this.fontSize * .8;
let gx = - this.fontSize + ( this.fontSize - w ) / 2;
let gy = ( this.cursorY + 1 ) * this.fontSize + ( this.fontSize - h ) / 2 + 1;
cc.beginPath();
cc.moveTo( gx, gy );
cc.lineTo( gx + w, gy + h / 2 );
cc.lineTo( gx, gy + h );
cc.closePath();
cc.fillStyle = "black";
cc.fill();
}
cc.restore();
}
}
class MessageWindow extends CmdElement {//c
constructor( app ) {
super( 1, 13, 19, 5, 12, app );
}
write( message, metronome, aizu ) {//m
this.message = message;
this.metronome = metronome;
this.aizu = aizu;
this.seek = 0;
this.app.anms.push( function() {
//check.
if( ! this.metronome.changed ) return;
this.seek ++;
//check.
if( this.seek == this.message.length ) {
return true;
}
}.bind( this ) );
}
typeKey( key ) {
if( this.seek < this.message.length ) {
//文字出力中のときは文字出力を一気に行う
this.seek = this.message.length - 1;
} else {
//文字出力終了のときは、呼び出し元に処理終了を知らせる。
this.aizu();
}
}
draw( cc ) {//m
super.draw( cc );
cc.save();
cc.translate( this.x, this.y );
cc.font = this.fontSize + "px ''";
cc.fillStyle = "black";
cc.fillText( this.message.substr( 0, this.seek ), 0, this.fontSize );
cc.restore();
}
}
class Screen {//c
constructor( app ) {//m
this.app = app;
this.actions = new Array();
this.style = {
visibility : false,
}
}
start() {
}
actionsPush() {//m
this.actions.push( Array.from( arguments ).join( "," ) );
}
searchStory() {//m
let story = null;
for( let i = this.actions.length - 1; i >= 0; i-- ) {
let action = this.actions[ i ];
if( story = this.stories[ action ] ) break;
if( story = this.sysStories[ action ] ) break;
}
this.actions.length = 0;
return story;
}
senseKey( key ) {
}
typeKey( key ) {
}
draw( cc ) {
}
}
class CmdScreen extends Screen {//c
constructor( app ) {//m
super( app );
this.style.visibility = false;
this.elements = new Array();
this.menus = new Array();
}
start( menusrc ) {//m
this.topMenu = this.openMenu( menusrc, 1, 1 );
}
addMenu( menu ) {
this.menus.push( menu );
this.addElement( menu );
}
addElement( element ) {//m
this.elements.push( element );
this.currentElement = element;
//check.
if( element instanceof Menu ) {
this.menus.push( element );
}
}
hitAnyKey() {
return new Promise( function( aizu ) {
let onkeydown_bak = onkeydown;
let onkeyup_bak = onkeyup;
onkeydown = function( e ) {
onkeydown = onkeydown_bak;
onkeyup = onkeyup_bak;
aizu();
}
}.bind( this ) );
}
openMessage( message ) {//m
return new Promise( function( aizu ) {
let hukidasi = new Hukidasi();
let metronome = this.app.addMetronome( "test1", 50 );
hukidasi.tell( message, aizu, metronome );
this.app.anms.push( hukidasi.anm.bind( hukidasi ) );
hukidasi.parent = this.currentMenu;
this.addElement( hukidasi );
this.currentMenu = hukidasi;
}.bind( this ) );
}
openMenu( menusrc, col, row ) {//m
let menu = new Menu( menusrc, col, row, this.app )
menu.parent = this.currentMenu;
this.addMenu( menu );
this.currentMenu = menu;
return menu;
}
execMenu() {//m
let script;
//メニュー順に引数を配置する方式
let args = new Array();
for( let menu of this.menus ) {
if( menu.selectedItem.script ) script = menu.selectedItem.script;
args.push( menu.selectedItem );
}
script.call( this.app, ...args );
}
closeElement( element ) {//m
this.elements.splice( this.elements.indexOf( element ), 1 );
//check.
if( element instanceof Menu ) {
this.menus.splice( this.menus.indexOf( element ), 1 );
}
if( this.elements.length ) {
this.currentElement = this.elements[ this.elements.length - 1 ];
} else {
this.app.currentScreen = this.app.map;
}
}
typeKey( key ) {//m
this.currentElement.typeKey( key );
}
draw( cc ) {//m
for( let element of this.elements ) {
element.draw( cc );
}
}
}
class MapScreen extends Screen {//c
/*
マップの表示という意味ではなく、
フィールド画面の総合的処理という意味。
*/
constructor( app ) {//m
super( app );
this.cellSize = 16;
this.cols = 17;
this.rows = 15;
this.colsHalf = Math.floor( this.cols / 2 );
this.rowsHalf = Math.floor( this.rows / 2 );
this.scroll = {
count : 0,
isAnimating : false,
tweakX : 0,
tweakY : 0,
}
this.keyLock = false;
}//constructor
treatX( x ) {
//tweak. ループ時は範囲外の値を範囲内に調整する
if( this.cfg.maploop ) {
if( x < 0 )
x += this.mapWidth;
else if( x >= this.mapWidth )
x -= this.mapWidth;
}
return x;
}
treatY( y ) {
//tweak. ループ時は範囲外の値を範囲内に調整する
if( this.cfg.maploop ) {
if( y < 0 )
y += this.mapHeight;
else if( y >= this.mapHeight )
y -= this.mapHeight;
}
return y;
}
typeKey( key ) {//m
if( key == 32 ) {
this.app.artz.soundz.pushA.rapidPlay();
this.app.cmd.style.visibility = true;
this.app.currentScreen = this.app.cmd;
let menu = new Menu( this.app.menusrcz.camp, 1, 1, this.app );
this.app.cmd.addElement( menu );
}
}
senseKey( key ) {//m
//check.
if( this.scroll.isAnimating || this.keyLock ) return;
let addX = ( key == 39 ) - ( key == 37 );
let addY = ( key == 40 ) - ( key == 38 );
//check. 移動先は壁
let sx = this.treatX( this.sx + addX );
let sy = this.treatY( this.sy + addY );
if( this.mapBits[ sy ] )
if( this.mapBits[ sy ][ sx ] )
if( this.cfg.walls.indexOf( this.mapBits[ sy ][ sx ] ) > -1 ) {
this.app.artz.soundz.pushWall.play();
return;
}
//1マススクロールの起動
if( addX || addY ) {
this.scroll.addX = addX;
this.scroll.addY = addY;
this.scroll.count = 0;
this.scroll.isAnimating = true;
}//if 一歩移動
}//senseKey
frameForScroll() {//m
//1マススクロールにおける、1ドット分のスクロール処理
this.scroll.tweakX -= this.scroll.addX;
this.scroll.tweakY -= this.scroll.addY;
//tweakX,Yが1ドットスクロールの主要のしくみ
//それについてはthis.draw()を参照してください。
this.scroll.count ++;
//check. スクロールの終了
if( this.scroll.count == this.cellSize ) {
this.scroll.isAnimating = false;
this.scroll.tweakX = 0;
this.scroll.tweakY = 0;
//draw()はスクロール終了後も参照するからここで0にする。
this.sx = this.treatX( this.sx + this.scroll.addX );
this.sy = this.treatY( this.sy + this.scroll.addY );
//マップ外に乗ったストーリー発生
if( ! this.cfg.maploop
&& ( this.sx < 0 || this.sx >= this.mapWidth
|| this.sy < 0 || this.sy >= this.mapHeight )
) {
this.actionsPush( "outer,got_on" );
} else {
//座標に乗ったストーリー発生
this.actionsPush( this.sx, this.sy, "got_on" );
}
}//if スクロール終了
}
playerMapMoveTo_fx( mapdatafunc /*other args*/ ) {//m
//視覚効果付きのマップ間移動
let argArray = Array.from( arguments );
//check. 歩く音
let donotFootsteps = false;
if( ( typeof argArray[ argArray.length - 1 ] ) == "boolean" ) {
donotFootsteps = argArray.pop();
}
if( ! donotFootsteps ) this.app.artz.soundz.walk.play();
this.keyLock = true;
//クロージャ(関数が終わっても、この関数内で定義した関数内では生き続ける変数)
let countMax = 3;
let count = countMax;
let flg = true;
//マップ間移動時の画面フェードアウト、イン
let metronome = this.app.addMetronome( "forFade", 125 );
this.app.anms.push( function() {
//check. メトロノームに合わせて
if( ! metronome.changed ) return;
if( flg ) {
//フェードアウト
count --;
let value = 100 * count / countMax;
this.app.cc.canvas.style.filter = "brightness( " + value + "% ) ";
//check. フェードアウトの終了
if( ! count ) {
//マップ間移動する
this.playerMapMoveTo( ...argArray );
flg = false;
count = countMax;
}
} else {
//フェードイン
count --;
let value = 100 * ( countMax - count ) / countMax;
this.app.cc.canvas.style.filter = "brightness( " + value + "% ) ";
//check. フェードインの終了(全終了)
if( ! count ) {
this.keyLock = false;
delete this.app.metronomes.forFade;
return true; //trueはアニメの終了の意
}
}
}.bind( this ) );
}
playerMapMoveTo( mapdatafunc /*other args*/ ) {//m
//マップ間移動
//check.
if( this.app.bgm ) {
this.app.bgm.pause();
this.app.bgm.currentTime = 0;
}
//データを記述した関数からデータを取り出す1
let commentData = mapdatafunc.toString().match( /\/\*\r\n([\s\S]+)\r\n\*\// )[ 1 ];
//データを記述した関数からデータを取り出す2
this.cfg = mapdatafunc();
//取り出したデータを使えるように加工
this.mapBits = new Array();
this.stories = new Object();
//マップデータを読み取る ここから
let lines = commentData.split( /\r\n/ );
for( let y = 0; y < lines.length; y++ ) {
let line = lines[ y ];
let bits = new Array();
for( let x = 0; x < line.length; x++ ) {
let bit = line.substr( x, 1 );
//check. 半角文字のときはストーリー設定あり
if( bit.match( /[0-9a-z ]/i ) ) {
let id = bit + line.substr( x + 1, 1 );
id = id.replace( / /, "" );
//check. エラー
if( ! this.cfg.shortcuts[ id ] ) {
alert( "マップ上に配置されたストーリー識別子が、ストーリー定義されていません。id:" + id );
bits.push( "!" );
continue;
}
//作業:ストーリーを座標情報をつけて複写
let stories = this.cfg.shortcuts[ id ].stories;
for( let key in stories ) {
let newkey = [ x, y, key ].join( "," );
this.stories[ newkey ] = stories[ key ];
}
//作業:半角文字をマップデータに直す
bit = this.cfg.shortcuts[ id ].bit;
//作業:座標情報をセット
this.cfg.shortcuts[ id ].x = x;
this.cfg.shortcuts[ id ].y = y;
x++;
}
bits.push( bit );
}
this.mapBits.push( bits );
}//for
//マップデータを読み取る ここまで
//BGM
if( this.cfg.bgmTitle ) {
this.app.bgm = this.app.artz.bgms[ this.cfg.bgmTitle ];
this.app.bgm.loop = true;
this.app.bgm.play();
}
//マップの外に出たときのストーリー
if( ! this.cfg.maploop ) {
let story = this.cfg.shortcuts.outer.stories.got_on;
this.stories[ "outer,got_on" ] = story;
}
//メソッドの引数によって処理を分ける(オーバーロードみたいに)
let argtypes;
let argArray = Array.from( arguments );
argtypes = argArray.map( arg => ( typeof arg ).substr( 0, 1 ) ).join( "" );
if( argtypes == "fs" ) {
//playerMapMoveTo( function, string )時
//移動先位置のショートカット指定
let shortcut = arguments[ 1 ];
this.sx = this.cfg.shortcuts[ shortcut ].x;
this.sy = this.cfg.shortcuts[ shortcut ].y;
} else {
//playerMapMoveTo( function, number, number )時
//移動先位置の座標指定
this.sx = arguments[ 1 ];
this.sy = arguments[ 2 ];
}
this.mapWidth = this.mapBits[ 0 ].length;
this.mapHeight = this.mapBits.length;
this.debugStories = new Object();
Object.keys( this.stories ).map( function( key ) {
let tokens = key.split( /,/ );
let x = tokens[ 0 ];
let y = tokens[ 1 ];
//check.
if( ! this.debugStories[ y ] )
this.debugStories[ y ] = new Object();
this.debugStories[ y ][ x ] = true;
}.bind( this ) );
}//playerMapMoveTo
draw( cc ) {//m
cc.clearRect( 0, 0, 256, 224 );
//this.sx,this.syが画面中央に来るように描画開始座標を調整
let sx = this.sx - this.colsHalf + this.mapWidth;
let sy = this.sy - this.rowsHalf + this.mapHeight;
//+ this.mapWidth,Heightは、下の計算で % this.mapWidth,Height
//としたときに負値になるのを避けるため。
cc.save();
cc.translate( -this.cellSize / 2 + this.scroll.tweakX, -this.cellSize / 2 + this.scroll.tweakY );
//tweakX,Yが描画位置を1ドットずつずらすので
//きれいに1ドットスクロールしてるようにみえる
cc.font = this.cellSize + "px ''";
cc.fillStyle = "rgb(0,255,0)";
for( let row = 0; row < this.rows; row++ ) {
let y = ( sy + row ) % this.mapHeight; //端を超えると反対側に
let gy = row * this.cellSize;
for( let col = 0; col < this.cols; col++ ) {
let x = ( sx + col ) % this.mapWidth; //端を超えると反対側に
let bit = this.mapBits[ y ][ x ];
let gx = col * this.cellSize;
let image = this.cfg.images[ bit ] ? this.cfg.images[ bit ] : bit;
cc.fillText( image, gx, gy + this.cellSize );
//debug.
if( 1 )
if( this.debugStories[ y ] )
if( this.debugStories[ y ][ x ] ) {
cc.strokeStyle = "red";
cc.strokeRect( gx, gy, this.cellSize, this.cellSize );
}
}
}
cc.restore();
//プレイヤーキャラ描画
cc.fillStyle = "red";
cc.font = this.cellSize + "px ''";
let gx = ( cc.canvas.width - this.cellSize ) / 2;
let gy = ( cc.canvas.height - this.cellSize ) / 2 + this.cellSize;
let sprite = this.app.artz.sprites.player;
let image = sprite.images[ sprite.metronome.index ];
cc.fillText( image, gx, gy );
}//draw
}//MapScreen
class TitleScreen extends Screen {//c
constructor( app ) {//m
super( app );
this.name = "title";
}
async typeKey( key ) {//m
this.app.artz.soundz.pushA.play();
//マップ画面を表示する。
let map = this.app.status.player.position.map;
let x = this.app.status.player.position.x;
let y = this.app.status.player.position.y;
this.app.screenz.map.playerMapMoveTo( map, x, y );
this.app.currentScreen = this.app.screenz.map;
this.app.screenz.map.style.visibility = true;
this.style.visibility = false;
await this.app.typeMessage( "スタート" );
}
draw( cc ) {//m
cc.clearRect( 0, 0, cc.canvas.width, cc.canvas.height );
cc.fillStyle = "white";
cc.fillText( "hit any key", 100, 100 );
}
}
App.worldmap = function() {/*
●〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃¥¥¥▲〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
¥山山山山山〃~~~~~~¥¥〃〃〃〃〃〃〃〃〃¥¥山山山¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
¥山山山〃〃〃〃~~~〃〃〃〃〃〃〃〃〃〃〃〃¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
¥山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
¥¥〃〃1 〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃〃〃山山〃〃〃〃〃〃〃¥¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃山山山〃〃〃〃〃〃〃¥¥¥山山山¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃山山山山山〃〃〃〃〃〃〃〃¥¥山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃山山山〃〃〃〃〃〃〃〃¥〃〃¥山山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃〃〃〃〃〃〃¥¥¥〃〃〃〃山山山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃〃〃〃〃〃〃〃¥〃〃〃〃〃山山山山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
山山山〃〃〃~~~〃〃〃〃〃〃〃〃〃〃山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
山山〃〃~~~~¥¥〃〃〃〃〃〃〃〃山山山山〃〃〃〃〃〃〃山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
山〃〃~~~¥¥¥〃〃〃〃〃〃〃〃山山山山山山山〃〃〃〃〃山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃¥¥¥¥〃〃〃〃〃〃〃〃〃山山山山山山山山〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃¥¥¥〃〃〃〃〃〃〃山山山山山山山山山山~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃¥¥¥〃〃〃〃〃〃山山山山山~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃¥¥¥〃〃〃〃〃〃〃〃〃〃~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃¥¥¥¥¥¥¥〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃¥¥¥¥¥¥¥¥〃〃〃〃〃〃〃〃~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃¥¥¥¥¥¥〃〃〃〃〃〃〃〃〃~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃¥¥¥¥¥〃〃〃〃〃〃〃〃〃〃~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃¥¥〃〃〃〃〃〃〃〃〃〃~~~~~〃〃〃〃〃〃〃¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃¥¥〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
〃〃〃〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
○〃〃〃〃〃〃〃〃~~~~~~~〃〃〃〃〃〃〃〃〃〃〃〃〃△〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃山山山山
*/
return {
maploop : true,
bgmTitle : "field",
walls : "~",
shortcuts : {
"1" : {
bit : "凸",
stories : {
"got_on" : {
begin : function( e ) {
this.screenz.map.playerMapMoveTo_fx( App.townmap, "s" );
},
},
},
},
},
images : {
"¥" : "🌲",
"凸" : "🏰",
"山" : "⛰",
"~" : "🌊",
},
}//return
}//worldmap
App.townmap = function() {/*
木木木木木木木〃・・〃木木木木木木木木木〃・・〃〃〃〃〃〃〃
木■■■■■木〃・・〃木■■■■■■■木〃・・〃〃〃〃〃〃〃
木■刀刀刀■木〃・・〃木■■■■■■■木〃・・〃〃〃〃〃〃〃
木■・ws・■木〃・・〃木■■■■■■■木〃・・〃〃〃〃〃〃〃
木■■□■■木〃・・〃木■■■扉■■■木〃・・〃〃〃〃〃〃〃
木木〃・剣木木〃・・〃〃花花花・花花花〃〃・・〃〃〃〃〃〃〃
s ・・・・・・・・・・・・・・・・・・・・・・・・・・・・・
・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・
木木盾・〃木木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
木■■□■■木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
木■・ss・■木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
木■盾盾盾■木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
木■■■■■木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
木木木木木木木〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
〃〃〃〃〃〃〃〃・・〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃〃
*/
return {
maploop : false,
bgmTitle : "town",
walls : "■□扉",
shortcuts : {
outer : {
bit : "〃",
stories : {
"got_on" : {
begin : function( e ) {
this.screenz.map.playerMapMoveTo_fx( App.worldmap, "1" );
},
},
},
},
"s" : {
bit : "・",
},
"ws" : {
bit : "👨🔧",
},
"ss" : {
bit : "👨🔧",
},
},
images : {
"木" : "🌳",
"花" : "🌷",
"扉" : "🚪",
"箱" : "📦",
"座" : "🪑",
"寝" : "🛏",
"∪" : "🏺",
"剣" : "⚔",
"刀" : "🗡",
"盾" : "🛡",
},
}//return
}//townmap
現実から離れた疑似体験が面白いんだと思います。
知らない世界を歩き、剣や魔法などの装備を整え、想像上のモンスターに出会い戦う。
数々の謎が行く手をはばみ、解き明かすと新しい展開に驚かされる。
もちろん、アクションゲームやシューティングゲームなど他のゲームも同じことが当てはまります。
自機を操りステージを縦横無尽に進むのは 現実から離れた疑似体験だし、武器を整えることもあるし、敵もいろいろ出てくる。
ステージの最後まで行きつくとボスキャラがいてその大きさや攻撃に驚かされ、ボスを倒せば次のステージのまた違った新しい様子に驚かされます。
でも RPG というのは細かいコマンドでさまざまなことができるので、より現実の生活に近いことができ、世界に気持ちを移しやすいのかもしれません。
なんといっても、ムード満点のBGMを流しながら、世界を歩く様子は、ドラマを感じずにはいられません。(以下の動画)
この動画にはファルコムのゲームソフトの楽曲が使われていますが、著作権については以下のようになっています。
でも、まるで、「規約に違反し、公開ゲームソフトに利用している」かのように見えるので、誤解を招きそうではあります。
実際、JavaScript のプログラムリストの中で同楽曲のファイルを指定して演奏するようになっています。
しかし、それをゲームソフトとして公開していないので、規約の意図には違反していないと考えています。
(上のプログラムリストでは、公開時の著作権の問題を考えてBGMのファイル名を差し替えています)
我ながら微妙なことしてるな~
ちなみに動画中の画面スクロールは撮影の関係でぎこちなくなっていますが、実際はファミコンのようなスムーズスクロールになっています。
(訪問者のどんなニーズと この記事がつながるか)